知识概念

微服务并没有一个官方的定义, 我们可以通过传统单体应用架构和微服务架构应用的对比,来理解什么是微服务。

单体应用架构

传统打车软件架构图:
image.png

这种单体应用比较适合于小项目,优点是:
- 开发简单直接,集中式管理
- 基本不会重复开发
- 功能都在本地,没有分布式的管理开销和调用开销

当然它的缺点也十分明显,特别对于互联网公司来说:
- 开发效率低:所有的开发在一个项目改代码,递交代码相互等待,代码冲突不断
- 代码维护难:代码功能耦合在一起,新人不知道何从下手
- 部署不灵活:构建时间长,任何小修改必须重新构建整个项目,这个过程往往很长
- 稳定性不高:一个微不足道的小问题,可以导致整个应用挂掉
- 扩展性不够:无法满足高并发情况下的业务需求

微服务应用架构

微服务架构的设计思路不是开发一个巨大的单体式应用,而是将应用分解为小的、互相连接的微服务。

比如,前面描述的系统可被分解为:
image.png
每个业务逻辑都被分解为一个微服务,微服务之间通过REST API通信。一些微服务也会向终端用户或客户端开发API接口。但通常情况下,这些客户端并不能直接访问后台微服务,而是通过API Gateway来传递请求。API Gateway一般负责服务路由、负载均衡、缓存、访问控制和鉴权等任务。

微服务架构优点:
- 解决了复杂性问题。它将单体应用分解为一组服务。虽然功能总量不变,但应用程序已被分解为可管理的模块或服务
- 体系结构使得每个服务都可以由专注于此服务的团队独立开发。只要符合服务API契约,开发人员可以自由选择开发技术。这就意味着开发人员可以采用新技术编写或重构服务,由于服务相对较小,所以这并不会对整体应用造成太大影响
- 微服务架构可以使每个微服务独立部署。这些更改可以在测试通过后立即部署。所以微服务架构也使得CI/CD成为可能

微服务架构问题及挑战

微服务的一个主要缺点是微服务的分布式特点带来的复杂性。开发人员需要基于RPC或者消息实现微服务之间的调用和通信,而这就使得服务之间的发现、服务调用链的跟踪和质量问题变得的相当棘手。

  1. 微服务的一大挑战是跨多个服务的更改
    - 比如在传统单体应用中,若有A、B、C三个服务需要更改,A依赖B,B依赖C。我们只需更改相应的模块,然后一次性部署即可。
    - 在微服务架构中,我们需要仔细规划和协调每个服务的变更部署。我们需要先更新C,然后更新B,最后更新A。
    2. 部署基于微服务的应用也要复杂得多
    - 单体应用可以简单的部署在一组相同的服务器上,然后前端使用负载均衡即可。
    - 微服务由不同的大量服务构成。每种服务可能拥有自己的配置、应用实例数量以及基础服务地址。就需要不同的配置、部署、扩展和监控组件。此外,我们还需要服务发现机制,以便服务可以发现与其通信的其他服务的地址

以上问题和挑战可大体概括为:
- API Gateway
- 服务间调用
- 服务发现
- 服务容错
- 服务部署
- 数据调用

秒懂微服务: https://www.kancloud.cn/owenwangwen/open-capacity-platform/1480155

微服务框架

如何应对上述挑战,出现了如下微服务领域的框架:
- Spring Cloud(各个微服务基于Spring Boot实现)
- Dubbo
- Service Mesh
- Linkerd
- Envoy
- Conduit
- Istio

image.png

了解Spring Cloud

官档: https://spring.io
核心项目及组件:https://spring.io/projects

#Spring Cloud和Dubbo对比

服务注册中心 Zookeeper Spring Cloud Netflix Eureka
核心要素 Dubbo Spring Cloud
服务调用方式 RPC REST API
服务监控 Dubbo-monitor Spring Boot Admin
断路器 不完善 Spring Cloud Netflix Hystrix
服务网关 Spring Cloud Netflix Zuul
分布式配置 Spring Cloud Config
服务跟踪 Spring Cloud Sleuth
消息总线 Spring Cloud Bus
数据流 Spring Cloud Stream
批量任务 Spring Cloud Task
…… …… ……

从上图可以看出其实Dubbo的功能只是Spring Cloud体系的一部分。

首先DubboSOA时代的产物,它的关注点主要在于服务的调用,流量分发、流量监控和熔断。而Spring Cloud诞生于微服务架构时代,考虑的是微服务治理的方方面面,另外由于依托了SpirngSpirng Boot的优势之上,两个框架在开始目标就不一致,Dubbo定位服务治理、Spirng Cloud是一个生态。

Spring Boot交付实践

创建项目

#打开IntelliJ IDEA,依次点击File > New > Project > Spring Initializr

#配置Project Metadata
image.png

#配置Dependencies,Web中Spring web和Template Engines中Thymeleaf,版本2.3.7
image.png

#配置maven
默认使用IDE自带的maven,下载较慢,可以换成阿里云仓配置。

下载地址:
链接: https://pan.baidu.com/s/1z9dRGv_4bS1uxBtk5jsZ2Q 提取码: 3gva
解压后放到D:\software\apache-maven-3.6.3,修改D:\software\apache-maven-3.6.3\conf\settings.xml 文件:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
  5. <localRepository>D:\opt\maven-repo</localRepository>
  6. <pluginGroups>
  7. </pluginGroups>
  8. <proxies>
  9. </proxies>
  10. <servers>
  11. </servers>
  12. <mirrors>
  13. <mirror>
  14. <id>alimaven</id>
  15. <mirrorOf>central</mirrorOf>
  16. <name>aliyun maven</name>
  17. <url>http://maven.aliyun.com/nexus/content/repositories/central/</url>
  18. </mirror>
  19. <mirror>
  20. <id>nexus-aliyun</id>
  21. <mirrorOf>*</mirrorOf>
  22. <name>Nexus aliyun</name>
  23. <url>http://maven.aliyun.com/nexus/content/groups/public</url>
  24. </mirror>
  25. </mirrors>
  26. </settings>

依次点击 Files > Settings > Maven
image.png

#启动项目
image.png

#访问本地服务:localhost:8080
image.png

编写代码功能

#创建controller(包)及HelloController.java(类)
image.png

HelloController.java

package com.example.demo.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @RequestMapping(value = "/hello", method = RequestMethod.GET)
    public String hello(String name) {
        return "Hello, " + name;
    }
}

#重启项目,带参数访问 http://localhost:8080/hello?name=crab
image.png

完善界面

#在resources/templates/目录下新建index.html

<!DOCTYPE html>
<html>
<head>
    <title>Devops</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<div class="container">
    <h3 th:text="${requestname}"></h3>
    <a id="rightaway" href="#" th:href="@{/rightaway}" >立即返回</a>
    <a id="sleep" href="#" th:href="@{/sleep}">延时返回</a>
</div>
</body>
</html>

#完善HelloController.java内容

package com.example.demo.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;

@RestController
public class HelloController {

    @RequestMapping(value = "/hello", method = RequestMethod.GET)
    public String hello(String name) {
        return "Hello, " + name;
    }

    @RequestMapping("/")
    public ModelAndView index(ModelAndView mv) {
        mv.setViewName("index");
        mv.addObject("requestname", "This is index");
        return mv;
    }

    @RequestMapping("/rightaway")
    public ModelAndView returnRightAway(ModelAndView mv) {
        mv.setViewName("index");
        mv.addObject("requestname","This request is RightawayApi");
        return mv;
    }

    @RequestMapping("/sleep")
    public ModelAndView returnSleep(ModelAndView mv) throws InterruptedException {
        Thread.sleep(2*1000);
        mv.setViewName("index");
        mv.addObject("requestname","This request is SleepApi"+",it will sleep 2s !");
        return mv;
    }
}

#重启项目访问
image.png

接入CI/CD流程

mvn环境镜像制作

将maven3集成到Jenkins slave所使用的tools镜像中
#将maven包解压到目录并按需配置对应settings.xml文件
conf/settings.xml

<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
  <localRepository>/opt/maven-repo</localRepository>

  <pluginGroups>
  </pluginGroups>

  <proxies>
  </proxies>

  <servers>
  </servers>

  <mirrors>
      <mirror>
            <id>alimaven</id>
            <mirrorOf>central</mirrorOf>
            <name>aliyun maven</name>
            <url>http://maven.aliyun.com/nexus/content/repositories/central/</url>
        </mirror>
        <mirror>
            <id>nexus-aliyun</id>
            <mirrorOf>*</mirrorOf>
            <name>Nexus aliyun</name>
            <url>http://maven.aliyun.com/nexus/content/groups/public</url>
        </mirror>
  </mirrors>

</settings>

Dockerfile

FROM alpine
LABEL maintainer="kazihuo8@qq.com"
USER root
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories && \
    apk update && \
    apk add  --no-cache openrc docker git curl tar gcc g++ make \
    bash shadow openjdk8 python2 python2-dev py-pip openssl-dev libffi-dev \
    libstdc++ harfbuzz nss freetype ttf-freefont chromium chromium-chromedriver && \
    mkdir -p /root/.kube && \
    usermod -a -G docker root
COPY config /root/.kube/
COPY config.json /root/.docker/
COPY requirements.txt /

RUN pip install -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com -r requirements.txt 

RUN rm -rf /var/cache/apk/* && \
    rm -rf ~/.cache/pip

#-----------------安装 kubectl--------------------#
COPY kubectl /usr/local/bin/
RUN chmod +x /usr/local/bin/kubectl
# ------------------------------------------------#

#---------------安装 sonar-scanner-----------------#
COPY sonar-scanner /usr/lib/sonar-scanner
RUN ln -s /usr/lib/sonar-scanner/bin/sonar-scanner /usr/local/bin/sonar-scanner && chmod +x /usr/local/bin/sonar-scanner
ENV SONAR_RUNNER_HOME=/usr/lib/sonar-scanner
# ------------------------------------------------#

#-----------------安装 maven--------------------#
COPY apache-maven-3.6.3 /usr/lib/apache-maven-3.6.3
RUN ln -s /usr/lib/apache-maven-3.6.3/bin/mvn /usr/local/bin/mvn && chmod +x /usr/local/bin/mvn
ENV MAVEN_HOME=/usr/lib/apache-maven-3.6.3
#------------------------------------------------#

打镜像并推送到仓库
$ docker build . -t harbor.od.com/base/tools:v4 -f Dockerfile
$ docker push harbor.od.com/base/tools:v4

jenkins slave配置修改

#更新Jenkins中的jnlp-slave-pod模板镜像
harbor.od.com/base/tools:v4

#新增挂载
由于镜像中maven的目录是/opt/maven-repo,而slave-pod是执行完任务后会销毁,因此需要将maven的数据目录挂载出来,不然每次构建都会重新拉取所有依赖的jar包
image.png

创建代码项目

#在gitlab上创建组(SpringCloud),在组下创建项目(springboot-demo)

#将上节交付实践中的代码推送到远程仓库

cd demo
git init
git remote add origin http://gitlab.crab.com/springcloud/springboot-demo.git
git add .
git commit -m "Initial commit"
git push -u origin master

#指定打出来的包名
#修改根目录下的pom.xml文件(第四行为增加行)

    </dependencies>

    <build>
      <finalName>${project.artifactId}</finalName><!--打jar包去掉版本号-->
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

#在项目根目录添加Jenkinsfile文件和Dockerfile文件
Jenkinsfile

pipeline {
    agent { label 'jnlp-slave'}

    options {
    buildDiscarder(logRotator(numToKeepStr: '10'))
    disableConcurrentBuilds()
    timeout(time: 20, unit: 'MINUTES')
    gitLabConnection('gitlab')
  }

    environment {
        IMAGE_REPO = "harbor.od.com/app/spring"
        NAMESPACE = "demo"
        HOST = "springboot.crab.com"
    }

    stages {
        stage('git-log') {
            steps {
                script{
                    sh "git log --oneline -n 1 > gitlog.file"
                    env.GIT_LOG = readFile("gitlog.file").trim()
                }
                sh 'printenv'
            }
        }        
        stage('checkout') {
            steps {
                container('tools') {
                    checkout scm
                }
            }
        }
         stage('mvn-package') {
            steps {
                container('tools') {
                    script{
                        sh 'mvn clean package'
                    }
                }
            }
        }
        stage('CI'){
            failFast true
            parallel {
                stage('Unit Test') {
                    steps {
                        echo "Unit Test Stage Skip..."
                    }
                }
            }
        }
        stage('build-image') {
            steps {
                container('tools') {
                    retry(2) { sh 'docker build . -t ${IMAGE_REPO}:${GIT_COMMIT}'}
                }
            }
        }
        stage('push-image') {
            steps {
                container('tools') {
                    retry(2) { sh 'docker push ${IMAGE_REPO}:${GIT_COMMIT}'}
                }
            }
        }
        stage('deploy') {
            steps {
                container('tools') {
                    sh "sed -i 's#{{IMAGE_URL}}#${IMAGE_REPO}:${GIT_COMMIT}#g' deploy/*"
                    sh "sed -i 's#{{NAMESPACE}}#${NAMESPACE}#g' deploy/*"
                    sh "sed -i 's#{{INGRESS_SPRINGBOOTDEMO}}#${HOST}#g' deploy/*"
                    timeout(time: 1, unit: 'MINUTES') {
                        sh "kubectl apply -f deploy/;sleep 20;rm -rf deploy"
                    }
                }
            }
        }
    }
}

Dockerfile

FROM openjdk:8-jdk-alpine
COPY target/demo.jar app.jar
CMD [ "sh", "-c", "java -jar /app.jar" ]

#在项目根目录新建deploy目录,存放资源清单文件
项目地址:
https://gitee.com/crabluo/springboot-demo

项目构建及效果

在jenkins中添加对应的流水线,构建后,访问 http://springboot.crab.com/ ,效果如下:
image.png

Spring Cloud交付实践

本内容基于SpringBoot 2.3.6.RELEASE 和Spring Cloud Hoxton.SR9版本

微服务场景

image.png

image.png

Eureka服务注册中心交付

https://docs.spring.io/spring-cloud-netflix/docs/2.2.5.RELEASE/reference/html/

SpringCloud体系中,服务之间的调用是通过http协议进行调用的。而注册中心的主要目的就是维护这些服务的服务列表。

创建spring cloud项目三部曲:
- 引入依赖包
- 修改application.yml配置文件
- 启动类添加注解

新建项目

打开IDEA,依次点击 file → new → project → Spring Initializr
image.png
#配置Project Metadata
image.png
#选择版本
image.png
#配置项目目录
image.png
#删除蓝色区域无效文件
image.png
#pom.xml文件中引入依赖
参考官档说明:https://spring.io/projects/spring-cloud#overview
##spring-cloud依赖

<properties>
    <spring.cloud-version>Hoxton.SR9</spring.cloud-version>
</properties>
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring.cloud-version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

eureka-server依赖

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>

说明
服务配置加载插件时会报错 Missing artifact com.fasterxml.jackson.core:jackson-core:bundle:2.5.0 ,添加如下依赖配置即可解决

    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-core</artifactId>
        <version>2.5.0</version>
        <type>bundle</type>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.5.0</version>
        <type>bundle</type>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-annotations</artifactId>
        <version>2.5.0</version>
        <type>bundle</type>
    </dependency>

启动服务

配置文件
##默认的是application.properties文件,此内容用yml文件
##application.yml

server:
  port: 8761

eureka:
  client:
    service-url:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
    register-with-eureka: false
    fetch-registry: false
  instance:
    hostname: localhost

启动类EurekaApplication.java配置

package com.crab.eureka;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {

    public static void main(String[] args) {
        SpringApplication.run(EurekaApplication.class, args);
    }

}

右键run运行项目
image.png
#访问 http://localhost:8761/
image.png

添加认证

默认是直接访问页面,为了安全性,添加账号登陆页面

pom文件中添加依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

application.yml

server:
  port: 8761
eureka:
  client:
    service-url:
      defaultZone: http://${spring.security.user.name}:${spring.security.user.password}@${eureka.instance.hostname}:${server.port}/eureka/
    register-with-eureka: false
    fetch-registry: false
  instance:
    hostname: localhost
spring:
  security:
    user:
      name: ${EUREKA_USER:admin}
      password: ${EUREKA_PASS:admin}

服务注册

原始eureka是没有任何服务注册到数据里,所以新建服务并注册。服务名称为user-service

#服务部署
操作步骤按照上面新建eureka项目的流程来
image.png
image.png

pom.xml添加

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

application.yml(ps:此步骤可忽略,直接看下文给出的yml更完善)

server:
  port: 7000
eureka:
  client:
    serviceUrl:
      defaultZone: http://${EUREKA_USER:admin}:${EUREKA_PASS:admin}@localhost:8761/eureka/

启动类

package com.crab.user;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

//注意这里也可使用@EnableEurekaClient
//但由于springcloud是灵活的,注册中心支持eureka、consul、zookeeper等
//若写了具体的注册中心注解,则当替换成其他注册中心时,又需要替换成对应的注解了。
//所以 直接使用@EnableDiscoveryClient 启动发现。
//这样在替换注册中心时,只需要替换相关依赖即可。
@EnableDiscoveryClient
@SpringBootApplication
public class UserServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserServiceApplication.class, args);
    }
}

关闭csrf
##新版本security默认开启csrf,当项目user-service启动后向eureka注册会报错,如下:

c.n.d.s.t.d.RetryableEurekaHttpClient    : Request execution failed with message: com.fasterxml.jackson.databind.exc.MismatchedInputException: Root name 'timestamp' does not match expected ('instance') for type [simple type, class com.netflix.appinfo.InstanceInfo]

解决方式是在eureka server端关闭
##在eureka server项目中新建类WebSecurityConfig.java来关闭

package com.crab.eureka;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@EnableWebSecurity
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable(); //关闭csrf
        http.authorizeRequests().anyRequest().authenticated().and().httpBasic(); //开启认证
    }
}

页面显示完善
服务启动后会自动注册到eureka,但是自动注册到eureka中心的服务在页面显示不规则,不易辨认,故配置辨识信息

server:
  port: 7000
eureka:
  client:
    serviceUrl:
      defaultZone: http://${EUREKA_USER:admin}:${EUREKA_PASS:admin}@localhost:8761/eureka/
  instance:
    instance-id: ${eureka.instance.hostname}:${server.port}
    prefer-ip-address: true
    hostname: user-service
spring:
  application:
    name: user-service

服务启动后将自动注册到eureka,访问页面如下
image.png

知识补充
Eurake有一个配置参数eureka.server.renewalPercentThreshold,定义了renews 和renews threshold的比值,默认值为0.85。当server在15分钟内,比值低于percent,即少了15%的微服务心跳,server会进入自我保护状态

默认情况下,如果Eureka Server在一定时间内没有接收到某个微服务实例的心跳,Eureka Server将会注销该实例(默认90秒)。但是当网络分区故障发生时,微服务与Eureka Server之间无法正常通信,这就可能变得非常危险了,因为微服务本身是健康的,此时本不应该注销这个微服务。

Eureka Server通过“自我保护模式”来解决这个问题,当Eureka Server节点在短时间内丢失过多客户端时(可能发生了网络分区故障),那么这个节点就会进入自我保护模式。一旦进入该模式,Eureka Server就会保护服务注册表中的信息,不再删除服务注册表中的数据(也就是不会注销任何微服务)。当网络故障恢复后,该Eureka Server节点会自动退出自我保护模式。

自我保护模式是一种对网络异常的安全保护措施。使用自我保护模式,而让Eureka集群更加的健壮、稳定。
开发阶段可以通过配置:eureka.server.enable-self-preservation=false关闭自我保护模式。

生产阶段,理应以默认值进行配置。
至于具体具体的配置参数,可至官网查看:http://cloud.spring.io/spring-cloud-static/Finchley.RELEASE/single/spring-cloud.html#_appendix_compendium_of_configuration_properties

高可用

高可用:
- 优先保证可用性
- 各个节点都是平等的,1个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务
- 在向某个Eureka注册时如果发现连接失败,则会自动切换至其它节点,只要有一台Eureka还在,就能保证注册服务可用(保证可用性)

注意点:
- 多实例的话eureka.instance.instance-id需要保持不一样,否则会当成同一个
- eureka.instance.hostname要与defaultZone里的地址保持一致
- 各个eureka的spring.application.name相同
image.png
#高可用场景实现
##copy eureka项目目录,分别命名为eureka-ha-peer1和eureka-ha-peer2

说明
eureka-ha-peer1和eureka-ha-peer2都是相同配置,区别点就在于命名和端口,因为跑在同一台电脑上,所以端口进行了区别,故下文以peer1作为示例进行说明,标红部分为需要变动的标识点。

修改pom.xml
eureka-ha-peer1

修改配置文件application.yml,注意集群服务,需要各个eureka的spring.application.name相同
server:
port: ${EUREKA_PORT:8762}
eureka:
client:
service-url:
defaultZone: ${EUREKA_SERVER:http://${spring.security.user.name}:${spring.security.user.password}@peer1:8762/eureka/,http://${spring.security.user.name}:${spring.security.user.password}@peer2:8763/eureka/}
fetch-registry: true
instance:
instance-id: ${eureka.instance.hostname}:${server.port}
hostname: peer1
spring:
security:
user:
name: ${EUREKA_USER:admin}
password: ${EUREKA_PASS:admin}
application:
name: eureka-cluster

配置host
##因为都是在电脑本机,故将2服务都解析到本地
127.0.0.1 peer1 peer2

分别启动peer1和peer2
在先启动peer1的时候会报错连接不到peer2,这是正常现象,因为peer2还没启动,当peer1启动完后启动peer2,就正常了。

访问页面 peer1:8762 or peer2:8763
image.png

#当其他服务要连接到高可用的eureka,配置如下:

      defaultZone: http://${EUREKA_USER:admin}:${EUREKA_PASS:admin}@peer1:8762/eureka/,http://${EUREKA_USER:admin}:${EUREKA_PASS:admin}@peer2:8763/eureka/

Eureka服务注册中心K8S交付

分析

eureka高可用互相注册,前提是每个节点的访问地址都是固定的。为了在K8S环境中保证服务的访问需求,使用statefulset管理。
image.png

statefulset管理有状态服务示例

此示例是为了演示statefulset服务的创建和使用(服务之间的访问)等。

nginx-sts.yaml

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: nginx-statefulset
  labels:
    app: nginx-sts
spec:
  replicas: 3
  serviceName: "nginx"
  selector:
    matchLabels:
      app: nginx-sts
  template:
    metadata:
      labels:
        app: nginx-sts
    spec:
      containers:
      - name: nginx
        image: nginx:alpine
        ports:
        - containerPort: 80

无头服务Headless Service
#ng-headless.yaml

kind: Service
apiVersion: v1
metadata:
  name: nginx
spec:
  selector:
    app: nginx-sts
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
  clusterIP: None

应用
]# kubectl apply -f .
]# kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-statefulset-0 1/1 Running 0 2m6s
nginx-statefulset-1 1/1 Running 0 83s
nginx-statefulset-2 1/1 Running 0 45s

pod之间的访问
]# kubectl exec -it nginx-statefulset-0 sh
/ # curl nginx-statefulset-1.nginx

说明
Pod之间的访问后面为啥加了.nginx,是因为直接访问pod name不行,加.nginx是因为无头服务配置里的name是nginx。

eureka应用

eureka-statefulset.yaml

# eureka-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: eureka-cluster
  namespace: dev
spec:
  serviceName: "eureka"
  replicas: 3
  selector:
    matchLabels:
      app: eureka-cluster
  template:
    metadata:
      labels:
        app: eureka-cluster
    spec:
      containers:
        - name: eureka
          image: 172.21.51.67:5000/spring-cloud/eureka-cluster:v1
          ports:
            - containerPort: 8761
          resources:
            requests:
              memory: 400Mi
              cpu: 50m
            limits:
              memory: 2Gi
              cpu: 2000m
          env:
            - name: MY_POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: JAVA_OPTS
              value: -XX:+UnlockExperimentalVMOptions
                -XX:+UseCGroupMemoryLimitForHeap
                -XX:MaxRAMFraction=2
                -XX:CICompilerCount=8
                -XX:ActiveProcessorCount=8
                -XX:+UseG1GC
                -XX:+AggressiveOpts
                -XX:+UseFastAccessorMethods
                -XX:+UseStringDeduplication
                -XX:+UseCompressedOops
                -XX:+OptimizeStringConcat
            - name: EUREKA_SERVER
              value: "http://admin:admin@eureka-cluster-0.eureka:8761/eureka/,http://admin:admin@eureka-cluster-1.eureka:8761/eureka/,http://admin:admin@eureka-cluster-2.eureka:8761/eureka/"
            - name: EUREKA_INSTANCE_HOSTNAME
              value: ${MY_POD_NAME}.eureka
            - name: EUREKA_PORT
              value: "8761"

无头服务,供Pod之间的访问
eureka-headless-service.yaml

apiVersion: v1
kind: Service
metadata:
  name: eureka
  namespace: dev
  labels:
    app: eureka
spec:
  ports:
    - port: 8761
      name: eureka
  clusterIP: None
  selector:
    app: eureka-cluster

有头服务,可通过ingress访问

apiVersion: v1
kind: Service
metadata:
  name: eureka-ingress
  namespace: dev
  labels:
    app: eureka-cluster
spec:
  ports:
    - port: 8761
      name: eureka-cluster
  selector:
    app: eureka-cluster
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: eureka-cluster
  namespace: dev
spec:
  rules:
    - host: eureka-cluster.crab.com
      http:
        paths:
          - backend:
              serviceName: eureka-ingress
              servicePort: 8761
            path: /
status:
  loadBalancer: {}

接入CICD流程

说明:下文基于前文在ideaJ中新建并配置 eureka 服务且能成功启动的基础上进行。

配置项目文件

所需要的文件:

  • Jenkinsfile
  • Dockerfile
  • deploy/statefulset.yaml,service.yaml
  • sonar-project.properties

在pom.xml中重写jar包名称

    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
                    ...
        </plugins>
    </build>

使得打出来的包和项目名称一致
##因为pom.xml文件中有 eureka ,故打出的包名为 eureka.jar

在项目根目录下新建Dockerfile

FROM openjdk:8-jdk-alpine
ADD target/eureka.jar app.jar
ENV JAVA_OPTS=""
CMD [ "sh", "-c", "java $JAVA_OPTS -jar /app.jar" ]

在项目根目录下新建Jenkinsfile

@Library('crab-devops') _

pipeline {
    agent { label 'jnlp-slave'}
    options {
        timeout(time: 20, unit: 'MINUTES')
        gitLabConnection('gitlab')
    }
    environment {
        IMAGE_REPO = "harbor.od.com/spring-cloud/eureka-cluster"
        IMAGE_CREDENTIAL = "credential-registry"
        DINGTALK_CREDS = credentials('dingTalk')
        PROJECT = "eureka-cluster"
    }
    stages {
        stage('checkout') {
            steps {
                container('tools') {
                    checkout scm
                }
            }
        }
        stage('mvn-package') {
            steps {
                container('tools') {
                    script{
                        sh 'mvn clean package'
                    }
                }
            }
        }
        stage('CI'){
            failFast true
            parallel {
                stage('Unit Test') {
                    steps {
                        echo "Unit Test Stage Skip..."
                    }
                }
                stage('Code Scan') {
                    steps {
                        container('tools') {
                            script {
                               devops.scan().start()
                            }
                        }
                    }
                }
            }
        }

        stage('docker-image') {
            steps {
                container('tools') {
                    script{
                        devops.docker(
                            "${IMAGE_REPO}",
                            "${GIT_COMMIT}",
                            IMAGE_CREDENTIAL
                        ).build().push()
                    }
                }
            }
        }
        stage('deploy') {
            steps {
                container('tools') {
                    script{
                        devops.deploy("deploy",false,"deploy/statefulset.yaml").start()
                    }
                }
            }
        }
    }
    post {
        success {
            script{
                devops.notificationSuccess(PROJECT,"dingTalk")
            }
        }
        failure {
            script{
                devops.notificationFailure(PROJECT,"dingTalk")
            }
        }
    }
}

在项目根目录下新建deploy目录,用于存放资源清单,目录下新建文件statefulset.yaml head-service.yaml headless-service.yaml ingress.yaml ,四个yaml文件以模板身份存在,故其中使用变量以便捷适用于多环境。
statefulset.yaml

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: eureka-cluster
  namespace: {{NAMESPACE}}
spec:
  serviceName: "eureka"
  replicas: 3
  selector:
    matchLabels:
      app: eureka-cluster
  template:
    metadata:
      labels:
        app: eureka-cluster
    spec:
      containers:
        - name: eureka
          image: {{IMAGE_URL}}
          ports:
            - containerPort: 8761
          resources:
            requests:
              memory: 400Mi
              cpu: 50m
            limits:
              memory: 2Gi
              cpu: 2000m
          env:
            - name: MY_POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: JAVA_OPTS
              value: -XX:+UnlockExperimentalVMOptions
                -XX:+UseCGroupMemoryLimitForHeap
                -XX:MaxRAMFraction=2
                -XX:CICompilerCount=8
                -XX:ActiveProcessorCount=8
                -XX:+UseG1GC
                -XX:+AggressiveOpts
                -XX:+UseFastAccessorMethods
                -XX:+UseStringDeduplication
                -XX:+UseCompressedOops
                -XX:+OptimizeStringConcat
            - name: EUREKA_SERVER
              value: "http://admin:admin@eureka-cluster-0.eureka:8761/eureka/,http://admin:admin@eureka-cluster-1.eureka:8761/eureka/,http://admin:admin@eureka-cluster-2.eureka:8761/eureka/"
            - name: EUREKA_INSTANCE_HOSTNAME
              value: ${MY_POD_NAME}.eureka
            - name: EUREKA_PORT
              value: "8761"

headless-service.yaml

apiVersion: v1
kind: Service
metadata:
  name: eureka
  namespace: {{NAMESPACE}}
  labels:
    app: eureka
spec:
  ports:
    - port: 8761
      name: eureka
  clusterIP: None
  selector:
    app: eureka-cluster

head-service.yaml 想通过ingress访问eureka,需要使用有头服务

apiVersion: v1
kind: Service
metadata:
  name: eureka-ingress
  namespace: {{NAMESPACE}}
  labels:
    app: eureka-cluster
spec:
  ports:
    - port: 8761
      name: eureka-cluster
  selector:
    app: eureka-cluster
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: eureka-cluster
  namespace: dev
spec:
  rules:
    - host: {{INGRESS_EUREKA_CLUSTER}}
      http:
        paths:
          - backend:
              serviceName: eureka-ingress
              servicePort: 8761
            path: /
status:
  loadBalancer: {}

项目根目录下新建sonar-project.properties

sonar.projectKey=eureka-cluster
sonar.projectName=eureka-cluster
# if you want disabled the DTD verification for a proxy problem for example, true by default
# JUnit like test report, default value is test.xml
sonar.sources=src/main/java
sonar.language=java
sonar.tests=src/test/java
sonar.java.binaries=target/classes

修改项目中文件application.yml。部署k8s集群时,将eureka的集群地址通过参数的形式传递到pod内部,因此本地开发时,直接按照单点模式进行。上线后也不需特定修改。

server:
  port: ${EUREKA_PORT:8761}
eureka:
  client:
    service-url:
      defaultZone: ${EUREKA_SERVER:http://${spring.security.user.name}:${spring.security.user.password}@localhost:8761/eureka/}
    fetch-registry: true
    register-with-eureka: true
  instance:
    instance-id: ${eureka.instance.hostname}:${server.port}
    hostname: ${EUREKA_INSTANCE_HOSTNAME:localhost}
    prefer-ip-address: true
spring:
  security:
    user:
      name: ${EUREKA_USER:admin}
      password: ${EUREKA_PASS:admin}
  application:
    name: eureka-cluster

说明:${EUREKA_PORT} ${EUREKA_SERVER}等都是从环境变量中读取,能读取的前提是在资源清单statefulset.yaml文件中配置了对应的变量和值。

提交项目到代码仓库

指定group下新建项目,名为eureka
image.png
#点击创建后,根据提示完成项目推送,为区别多分支情况,此内容将推送到develop分支
cd existing_folder #进入到eureka项目根目录
git init
git remote add origin http://gitlab.crab.com/springcloud/eureka.git
git add .
git commit -m “Initial commit”
git checkout -b develop
git push —set-upstream origin develop

刷新项目页面,可看到项目已推送到代码仓库的develop分支
image.png

新建jenkins项目

新建项目,选择多分支流水线,配置如下
image.png
image.png

项目效果

将项目对应的jenkins项目配置好后会自动触发构建,构建成功后配置对应的域名解析 eureka-cluster.crab.com
访问效果如下(账号/密码:admin/admin):
image.png