微服務實踐(二):微服務與服務容器化
軟體架構
軟體架構是在軟體的內部,經過綜合各種因素的考量、權衡,選擇特定的技術,將系統劃分成不同的部分並使這些部分相互分工,彼此協作,為使用者提供需要的價值。
軟體架構影響因素
- 業務需求:需要實現的功能
- 技術棧:選擇用於實現功能的技術
- 成本:願意為開發軟體付出的價值
- 組織架構:有哪些部門能為開發提供幫助
- 可擴充套件性:面向擴充套件開放
- 可維護性:維護成本
軟體架構進化
單體架構
單體架構功能、業務集中在一個釋出包裡,部署執行在同一個程序中。
- 優勢:易於開發、測試、部署、水平擴充套件
- 劣勢:程式碼膨脹而難以維護,構建、部署成本大,上手週期長,升級現有技術棧困難,對資源的不同需求導致擴充套件性差
微服務架構
使用一套小服務來開發單個應用的方式,每個服務執行在獨立的程序裡,一般採用輕量級的通訊機制互聯,並且它們可以通過自動化的方式部署。
微服務
誕生背景
- 網際網路行業快速發展
- 敏捷開發,精益方法深入人心
- 容器技術的成熟
特徵
- 單一職責:只把緊密相關的業務放在一起,無關的業務獨立出去
- 輕量級通訊:微服務之間的通訊應該是輕量級的,平臺無關,語言無關
- 隔離性:每個微服務執行在自己的程序中,不會相互干擾
- 有自己的資料:有獨立的資料儲存系統
- 技術多樣性:開發人員選用自己擅長的技術棧
優勢
- 獨立性:微服務構建、部署、擴容、縮容、容錯甚至資料庫都是獨立的
- 敏捷性:微服務功能單一,API簡單,迭代方便
- 技術棧靈活:在保證提供穩定服務的情況下微服務各個模組可以自由選擇技術棧
- 高效團隊:每個團隊負責自己的微服務,提高開發效率
不足
- 額外工作:微服務需要考慮如何拆分系統,而單體架構不需要
- 資料一致性:單體架構可能只有一個數據庫,通過事務可以保證資料一致性,而微服務有多個數據庫
- 溝通成本:服務的提供方和呼叫方可能屬於多個團隊,增加了溝通成本
微服務架構引入的問題
通訊
從通訊協議角度考慮
選擇RPC框架考慮因素
- I/O、執行緒排程模型
- 序列化方式
- 是否需要多語言支援
常見RPC框架

dubbo
ofollow,noindex">Dubbo 是一款高效能的RPC框架。註冊中心提供註冊和發現服務,而不提供轉發請求。消費者訂閱提供者在註冊中心註冊的服務,通過介面呼叫提供者對服務的具體實現。監控中心服務統計消費者和提供者的呼叫次數、呼叫時間等,並在彙總後發給註冊中心。

motan
是新浪微博開源的一個Java RPC框架,目前在微博平臺中已經廣泛應用,每天為數百個服務完成近千億次的呼叫。

thrift
是一個跨語言的服務部署框架,通過IDL(Interface Definition Language,介面定義語言)來定義RPC的介面和資料型別,並通過thrift編譯器生成不同語言的程式碼,並由生成的程式碼負責RPC協議層和傳輸層的實現。

grpc
在gRPC裡客戶端應用可以像呼叫本地物件一樣直接呼叫另一臺不同的機器上服務端應用的方法。在服務端實現介面,並執行一個gRPC伺服器處理客戶端呼叫。在客戶端擁有一個存根能夠像服務端一樣呼叫方法。gRPC同樣是跨語言的,使用protobuf進行序列化。

rpc框架對比
發現
-
客戶端發現
客戶端發現
-
服務端發現
服務端發現
部署
傳統部署可能通過jenkins指令碼上傳程式碼來部署,微服務隨著數量增多,傳統部署方式部署困難,需要引入服務編排工具,如Mesos、Docker Swarm、Kubernetes。
分散式服務實踐
以一個簡單的課程服務為例,實現使用者登入、註冊,傳送訊息驗證碼,查詢課程資訊等功能,整合dubbo、thrift、SpringBoot、SpringCloud等框架,並在Docker上進行容器化部署。

服務架構
- course-thrift-server:基於thrift(Java),通過TSocket對外提供使用者相關服務。
- course-dubbo-server:基於Dubbo,通過註冊中心zookeeper註冊提供的服務。
- course-message:基於thrift(golang),通過TSocket對外提供資訊相關服務。
- course-web:web服務入口,根據業務邏輯進行RPC呼叫。
- course-api-gateway:基於SpringCloud zuul的服務閘道器,通過REST呼叫course-web的服務,面向客戶呼叫。

服務依賴
使用者服務
使用者服務負責查詢和註冊使用者資訊,實現thrift檔案生成的java介面,並通過TSocket對外暴露服務。
thrift程式碼生成
編寫thrift檔案( thrift語法 ),下載thrift程式碼生成工具,使用命令 thrift --gen java --out ../course-interface/src/main/java user_model.thrift && thrift --gen java --out ../course-interface/src/main/java user_service.thrift
生成thrift類和介面檔案。
namespace java com.wch.course.domain.thrift typedef i32 INT typedef i64 LONG /** * 使用者資訊 */ struct UserInfo { /** * 使用者編號 */ 1: optional INT id, /** * 使用者名稱 */ 2: optional string username, /** * 密碼 */ 3: optional string password, /** * 真實姓名 */ 4: optional string realName, /** * 手機號 */ 5: optional LONG mobile, /** * 電子郵箱 */ 6: optional string email }
namespace java com.wch.course.service.thrift include 'user_model.thrift' typedef i32 INT typedef user_model.UserInfo UserInfo /** * 使用者資訊服務 */ service IUserService { /** * 通過使用者編號查詢使用者資訊 */ UserInfo queryUserById(1: INT id); /** * 通過使用者名稱查詢使用者資訊 */ UserInfo queryUserByName(1: string username); /** * 新增使用者 */ void addUser(1: UserInfo userInfo); }
通過TSocket暴露thrift服務
@Slf4j @Component public class ThriftServer { @Value("${thrift.port}") private int thriftServicePort; @Resource private IUserService.Iface userService; @PostConstruct public void startThriftServer() { TProcessor processor = new IUserService.Processor<>(userService); TNonblockingServerSocket socket = null; try { socket = new TNonblockingServerSocket(thriftServicePort); } catch (TTransportException e) { log.error("start thrift server error", e); } TNonblockingServer.Args args = new TNonblockingServer.Args(socket); args.processor(processor); args.transportFactory(new TFramedTransport.Factory()); args.protocolFactory(new TBinaryProtocol.Factory()); TServer server = new TNonblockingServer(args); new Thread(new Runnable() { @Override public void run() { log.info("thrift server start on {} ...", thriftServicePort); server.serve(); } }).start(); } }
課程服務
課程服務負責查詢課程資訊,實現課程服務並將Dubbo介面註冊到zookeeper。
使用dubbo-spring-boot-starter作為dubbo與SpringBoot整合的jar包(SpringBoot 1.x版本只支援dubbo-spring-boot-starter 0.1.1)。為了後期與docker整合,部分引數以佔位符的形式存在,而dubbo-spring-boot-starter的自動配置很早就從配置檔案中取固定格式的dubbo配置,此時部分引數還是佔位符的形式,導致自動配置失敗,因此本專案選擇自定義配置引數名稱,手動編寫部分配置替換自動配置。
- 服務端
配置檔案:
dubbo: scan: basePackages: com.wch.course.service.impl application: name: dubbo-provider provider: port: ${dubbo.port} registry: zookeeper://${registry.address}?client=curator
配置註冊中心和服務項:
@Configuration public class DubboConfig { @Value("${dubbo.registry}") private String registry; @Value("${dubbo.provider.port}") private int dubboPort; @Bean public RegistryConfig registryConfig() { return new RegistryConfig(registry); } @Bean public ProtocolConfig dubboProtocolConfig() { return new ProtocolConfig("dubbo", dubboPort); } }
web服務
web服務負責根據業務邏輯進行RPC呼叫,並向外部暴露REST介面。
thrift客戶端
private TServiceClient getService(String host, int port, int timeout, Class<? extends TServiceClient> clazz) throws Exception { TSocket socket = new TSocket(host, port, timeout); TFramedTransport transport = new TFramedTransport(socket); transport.open(); TBinaryProtocol tBinaryProtocol = new TBinaryProtocol(transport); Constructor<? extends TServiceClient> constructor = clazz.getConstructor(TProtocol.class); return constructor.newInstance(tBinaryProtocol); } public IUserService.Client getUserService() throws Exception { return (IUserService.Client) getService(userServiceHost, userServicePort, timeout, IUserService.Client.class); } public IMessageService.Client getMessageService() throws Exception { return (IMessageService.Client) getService(messageServiceHost, messageServicePort, timeout, IMessageService.Client.class); }
dubbo客戶端配置
配置檔案:
dubbo: application: name: dubbo-consumer registry: zookeeper://${registry.address}?client=curator
配置註冊中心:
@Configuration public class DubboConfig { @Value("${dubbo.registry}") private String registry; @Bean public RegistryConfig registryConfig() { return new RegistryConfig(registry); } }
APIGateway
APIGateway基於SpringCloud zuul向外暴露REST介面。
配置檔案:
zuul: routes: course: path: /course/** url: http://localhost:8080/course-web/course/ user: path: /user/** url: http://localhost:8080/course-web/user/ message: path: /message/** url: http://localhost:8080/course-web/message/
構建docker映象
對各服務模組進行打包後編寫Dockerfile:
FROM openjdk:8-jre-slim LABEL maintainer="wch [email protected]" COPY target/course-web-1.0-SNAPSHOT.jar /course-web.jar ENTRYPOINT ["java", "-jar", "/course-web.jar"]
編寫docker-compose檔案
各映象構建完成後編寫docker-compose檔案來方便編排docker服務。
version: '3' services: zookeeper: image: zookeeper:3.4.13 networks: - rpc-bridge redis: image: redis:latest command: - "--appendonly yes" volumes: - d:/docker/redis:/data networks: - component-bridge mysql: image: mysql:5.7 environment: MYSQL_ROOT_PASSWORD: admin MYSQL_ROOT_HOST: "%" volumes: - d:/docker/mysql/data:/var/lib/mysql - d:/docker/mysql/conf/my.cnf:/etc/mysql/my.cnf networks: - data-bridge thrift-server: image: course-thrift-server:1.0 links: - mysql command: - "--spring.profiles.active=prd" - "--rpc.thrift.port=8081" - "--mysql.address=mysql:3306" networks: - rpc-bridge - data-bridge dubbo-server: image: course-dubbo-server:1.0 links: - zookeeper - mysql command: - "--spring.profiles.active=prd" - "--dubbo.port=8083" - "--registry.address=zookeeper:2181" - "--mysql.address=mysql:3306" networks: - rpc-bridge - data-bridge web: image: course-web:1.0 links: - redis - zookeeper - thrift-server volumes: - d:/docker/app/logs/course-web:/var/app/logs/course-web command: - "--spring.profiles.active=prd" - "--server.port=8080" - "--thrift.service.user.host=thrift-server" - "--thrift.service.user.port=8081" - "--redis.host=redis" - "--redis.port=6379" - "--registry.address=zookeeper:2181" networks: - rpc-bridge - component-bridge - net-bridge api-gateway: image: course-api-gateway:1.0 links: - web command: - "--spring.profiles.active=prd" - "--server.port=80" - "--web.host=web" - "--web.port=8080" ports: - 80:80 networks: - net-bridge networks: net-bridge: driver: bridge component-bridge: driver: bridge data-bridge: driver: bridge rpc-bridge: driver: bridge
執行和管理docker服務
# 啟動 docker-compose up -d # 對dubbo-server進行擴容 docker-compose up --scale dubbo-server=3 -d # 關閉並刪除所有容器 docker-compose down
故障
Docker for Windows暴露連線埠
本地使用Docker for Windows,執行docker version命令,客戶端報錯,需要勾選Expose daemon ontcp://localhost:2375 without TLS。

Docker for Windows暴露連線埠
Docker for Windows共享本地硬碟設定不生效
修改本地共享安全策略後選擇共享給Docker的硬碟。

Docker本地共享安全設定

Docker選擇共享本地硬碟
MySQL預設禁止遠端連線
MySQL預設禁止遠端連線,需要配置docker容器連線使用者的遠端連線許可權。
- 安裝的mysql
grant all privileges on *.* to 'root'@'%' identified by 'pwd'; flush privileges; # 檢視指定使用者是否改為不限制IP登入 SELECT user, host FROM mysql.user;
-
docker提供的mysql映象
docker啟動引數中使用-e MYSQL_ROOT_HOST=%
docker run -d -e MYSQL_ROOT_PASSWORD=pass -e MYSQL_ROOT_HOST=% -p 3307:3306 -v d:/docker/mysql/conf/my.cnf:/etc/mysql/my.cnf -v d:/docker/mysql/data:/var/lib/mysql --name=mysql mysql:5.7
redis-windows預設繫結本地
在redis配置檔案中,配置 bind 127.0.0.1 表示redis限制本地連線,windows版本預設是開啟的,docker容器進行連線時需要註釋此配置、
redis設定密碼錯誤
拉取的redis映象預設沒有配置密碼,若配置檔案中填寫了密碼進行連線會保錯。在上docker版本的配置檔案中通過配置預設值,即:
spring.redis.password=
來覆蓋密碼配置。
mysql容器ssl連線錯誤
使用java客戶端連線mysql容器出錯,seSSL引數修改為false。
jdbc:mysql://${mysql.address}/test?useUnicode=true&characterEncoding=utf8&useSSL=false