刚从零开始上线了一套基于SpringCloud的微服务应用,遇到了一些坑,在三月份的最后一天做一个总结

技术选型

首先讲下技术选型,在微服务领域两大巨头,Dubbo和SpringCloud,近期关注微服务的朋友们可能知道,例如Erueka,Hystrix,Zuul这些貌似都已经停止更新了,虽然他们官网上说,现有的版本已经满足现有的需求,但软件领域,睡一觉就是另一个样,但好在,Spring积极在解决这些问题,基本上也都有替代品,比如全套的阿里系微服务spring-cloud-alibaba。

为什么放弃dubbo这个微服务框架呢,首先第一点,dubbo基于RPC通信,类似thrift,用过thrift的朋友,都知道,虽然thrift这个框架非常厉害,它可以调用其他语言的服务,java调python啊,python调c啊这些,而且性能非常不错。但有一点,一旦你的接口有变动,那所有涉及到这个接口的实现服务,都需要重新生成一遍对应语言的接口并依赖新生成的接口完成实现,实在是非常麻烦

Dubbo没有thrift那么麻烦,但如果新加入了接口类,就一定要在提供方和调用方同时配置一堆东西,如果分组开发,由此产生的沟通成本将非常昂贵,我们项目要求快速迭代,因此,放弃Dubbo的RPC协议,使用基于Http的Sping Cloud体系,而且由业务推倒,后期服务可能不会过于复杂,因此,服务降级显得不那么重要,如果后期涉及相关问题,可以采用阿里套餐中[Sentinel]组建,以资源的方式来帮助我们处理服务降级相关的问题。

构建

为了践行OpsDev快速迭代,必然是要采用自动化部署的,选用的是老牌自动化部署工具[Jenkins],但也由此产生了一个小问题,Jenkins是从git上获取代码,本地编译后通过脚本部署的,但git无法拉取某个路径下的代码,所以只能吧项目分开,使用多个git仓库管理,原本项目是使用svn进行托管,微服务项目可以放在一个工程下,同时打开整个微服务工程,但现在看来没办法这么做了。

分享一下部署脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/usr/local/jdk1.8.0_191/bin:/usr/local/elasticsearch-6.5.4/bin:/root/bin
PATH=.:$PATH
export PATH

JENKINS_NAME=$1
ARTIFACT_ID=$2
SPRING_PROFILE=$3

echo $JENKINS_NAME $ARTIFACT_ID $SPRING_PROFILE

if [ ! -d "/data/webapp/$JENKINS_NAME" ]; then
mkdir /data/webapp/$JENKINS_NAME
fi

SING_STR="\$\\"
cd /data/webapp/deployScript
mv $JENKINS_NAME-0.0.1-SNAPSHOT.jar $JENKINS_NAME-0.0.1-SNAPSHOT_bak.jar

scp -P22 xxxx@xxx.xxx.xxx.xxx:"/data/webapp/.jenkins/jobs/$JENKINS_NAME/modules/com.thinkerjet$SING_STR$ARTIFACT_ID/lastSuccessful/archive/com.thinkerjet/$ARTIFACT_ID/0.0.1-SNAPSHOT/$ARTIFACT_ID-0.0.1-SNAPSHOT.jar" ./$JENKINS_NAME-0.0.1-SNAPSHOT.jar

spring_pid=`ps -ef | grep $JENKINS_NAME | grep -v grep | grep -v deploy_lexin_boot.sh| awk '{print $2}'`
[ -n "$spring_pid" ] && kill -9 $spring_pid

cp $JENKINS_NAME-0.0.1-SNAPSHOT.jar /data/webapp/$JENKINS_NAME/$JENKINS_NAME-0.0.1-SNAPSHOT.jar
cd /data/webapp/$JENKINS_NAME
/usr/bin/nohup java -jar $JENKINS_NAME-0.0.1-SNAPSHOT.jar --spring.profiles.active=${SPRING_PROFILE} > /data/webapp/$JENKINS_NAME/catalina.out 2>&1 &

jenkins来执行远程脚本,通过scp命令获取打包好的文件,然后执行一下就OK了,另外,不要用lsof -i 这个天杀的命令
它会把所有跟那个端口有通讯的进程都查出来,比如,你如果用lsof -i:eureka端口,那么你部署在这台机器上的其他微服务,全被kill了

开发

基于Greenwich.SR1版本开发
未完待续

遇到的坑

  1. 使用OpenFeign时,使用@FeignClient注解,会在Bean容器中生成Bean对象,如多个@FeignClient注释的类使用相同服务,无法启动

    • 解决方法:@FeignClient(name = “服务名称”, contextId = “类名”)
  2. 使用OpenFeign时,服务间调用默认1秒失败,会出现服务调用成功,但响应超时 - ReadTimeOut,使数据不一致

    • 解决方法:设置ribbon超时时间 (OpenFeign底层调用ribbon以及Hystrix),高版本中默认Hystrix不开启
      1
      2
      3
      ribbon:
      ReadTimeout: 60000
      ConnectTimeout: 60000
  3. 微服务部署时,需要先让路由getway停止路由到被部署服务,以防请求过程中服务重启

    • 解决方法:使用spring-boot-starter-actuator优雅关闭服务
    • 添加spring-boot-starter-actuator,默认shutdown等端点为关闭状态,启动shutdown端点,访问http:localhost:xxxx/actuator/shutdown
      1
      2
      3
      4
      5
      6
      7
      8
      9
      management:
      endpoint:
      shutdown:
      enabled: true
      endpoints:
      web:
      exposure:
      include: shutdown
      base-path: /
  4. 配置拦截器无法Autowired

    • 解决方法:使用WebApplicationContextUtils.getRequiredWebApplicationContext方法获取bean工厂,后加载
      1
      2
      3
      4
      5
      6
      private Foo foo;
      ...
      if (foo == null) {
      BeanFactory factory = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());
      foo = factory.getBean(Foo.class);
      }
  5. 日志输出

    • 方法:使用SpringBoot内置文件输出,注:该配置只应在生产环境中使用
      1
      2
      logging:
      path: 此处建议设置为服务器中jar路径下,新建工程同名文件夹,用来保存该项目日志
  6. 接口保护

    • 方法:在getway中配置2级路径,如不希望外界路由该接口,则在第一级路径区分,内部服务使用/provider,提供给外部的服务使用/api
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      spring:
      application:
      name: gateway-service
      profiles:
      active: dev
      cloud:
      gateway:
      routes:
      - id: xxxx
      uri: lb://business
      predicates:
      - Path=/business/**
      filters:
      - StripPrefix=1 //该配置表示如访问该服务,则不传递/business/路径
      //例如前台访问/businsess/foo, 则服务真实控制器路径为/foo
      - id: interface
      uri: lb://interface
      predicates:
      - Path=/api/** //表示只有/api/为一级路径的控制器才可以被路由访问
  7. 生产环境Eureka高可用,及unavailable-replicas问题

    • eureka.instance.appname必须与spring.application.name相同
    • eureka.instance.hostname必须使用hosts名称映射,修改hosts文件
    • eureka.client.fetch-registryeureka.client.register-with-eureka要么不写,要么必须为true

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      server:
      port: 8761
      eureka:
      instance:
      leaseRenewalIntervalInSeconds: 5
      leaseExpirationDurationInSeconds: 10
      server:
      enable-self-preservation: true
      evictionIntervalTimerInMs: 5000
      useReadOnlyResponseCache: false
      client:
      fetch-registry: true
      register-with-eureka: true
      spring:
      application:
      name: xxx
      profiles:
      active: prod-1
      logging:
      level:
      com.netflix: INFO
      ---
      server:
      port: 8761
      spring:
      profiles: prod-1
      eureka:
      instance:
      hostname: service-eureka-01
      appname: xxx
      client:
      serviceUrl:
      defaultZone: http://service-eureka-02:8762/eureka
      ---
      server:
      port: 8762
      spring:
      profiles: prod-2
      eureka:
      instance:
      hostname: service-eureka-02
      appname: xxx
      client:
      serviceUrl:
      defaultZone: http://service-eureka-01:8761/eureka

总结

以上是我个人线上用Spring cloud踩到的一些坑,记录一下