Zhu.Yang

朱阳的个人博客(公众号:think123)

0%

平常在写代码的过程中,我们经常需要记录日志,具体的日志实现我们可以使用logback, log4j, log4j2等。 但是一般我们会通过日志门面来记录日志,比如通过SLF4J或者apache commons logging。 无论使用哪种方式记录日志都不在我们这篇文章的讨论范围内。

如果你对它们如何找到真正的日志实现感兴趣,可以看看我之前的文章

日志怎么记录

日志分为6种,每种级别实际上有不同的应对场景,但是说实话我在项目中看到最多的永远是INFO, ERROR级别。

几乎没有怎么见到业务系统中有其他级别。 DEBUG的日志在集成的各个框架中是比较多见的。

日志应该是帮助我们定位问题的,所以日志怎么记录其实很关键。

阅读全文 »

最近项目中进行仓库拆分了之后,因为引入了公共包,所以就存在可能有snapshot版本以及release版本问题,比如我想要在dev环境的时候import snapshot版本,prod环境的时候又使用release版本,为了不频繁修改pom.xml文件,因此决定使用POM的profile来解决这个问题。

当然由于maven默认是不下载snapshot包的,因此我们要配置让它下载,这里分为全局配置和项目级别配置

项目配置

在pom文件中添加如下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<repositories>
<repository>
<id>nexus</id>
<!--修改成自己私服地址-->
<url>http://localhost:18081/repository/maven-public/</url>
<releases>
<enabled>true</enabled>
<updatePolicy>always</updatePolicy>
</releases>
<snapshots>
<!--主要是这里-->
<enabled>true</enabled>
<updatePolicy>always</updatePolicy>
</snapshots>
</repository>
</repositories>

阅读全文 »

最近要把项目中的子模块单独拆分为一个项目,并且移动到新的仓库地址,同时需要保留所有提交记录以及所有分支(包括未上线-未合并到master分支)的代码,我这里总结了2种方式以供大家参考。

项目现状

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bv_sc_server
|───bvpro-modules
│ ├─bvpro-job
│ │ ├─src
│ │ │ └─main
│ │ │ ├─java
│ │ │ │ └─com
│ │ │ │ └─bvpro
│ │ │ │ └─job
| ├─bvpro-file
│ │ ├─src
│ │ │ └─main
│ │ │ ├─java
│ │ │ │ └─com
│ │ │ │ └─bvpro
│ │ │ │ └─file

<!-more–>

项目中有很多子模块,我需要将他们单独独立出来放到放到一个新的仓库,同时只保留各自仓库的代码。

比如我有一个分支feature/migrate-data,它同时修改了bvpro-file以及bvpro-job的代码,那么迁移过去的效果希望是
新的bvpro-file和bvpro-job仓库都有这个分支,同时也只包含当前仓库的代码,而不再想之前一样混杂着多个模块的代码。

因此调研了下发现有两种方案可以达到这样的目的,一种是git subtree, 一种是git filter-repo。

git subtree

需要拆分的仓库叫做 bv_sc_server, 现在需要拆分的模块是 bv_sc_server/bvpro-modules/bvpro-job, 拆分后的新仓库叫做bvpro-job

  1. 先在代码服务器(比如gitlab)上新建一个空的仓库, 比如bvpro-job

  2. 在本地文件夹clone刚创建的新仓库, 和bv_sc_server在同一个目录下

1
git clone http://localhost:8090/sc_group/bvpro-job.git
  1. 进入bv_sc_server这个仓库的目录下进行拆分,当前处于master分支(这个分支是需要迁移的分支)
1
git subtree split -P bvpro-modules/bvpro-job -b feature/split-bvpro-job
  1. 进入到bvpro-job这个新仓库,执行以下命令:

到这里我们就把bv_sc_server中bvpro-job模块的master分支代码迁移到了新的仓库,但是其他分支还没有迁移过去,所以这里我们重复执行下操作

这里为了不相互影响,我们将本地bvpro-job目录删除,然后重新新建一个空白bvpro-job目录。然后在重复执行上面的命令,这里为了演示方便我就将命令写到一起。

1
2
3
4
5
6
7
8
9
10
11
12
cd bvpro-job
git clone http://localhost:8090/sc_group/bvpro-job.git
cd ../bv_sc_server
#切换到需要迁移的分支
git checkout feature/SCA-5034_AutoApprovalForTcAndSc
git subtree split -P bvpro-modules/bvpro-job -b split/SCA-5034_AutoApprovalForTcAndSc
cd ../bvpro-job
git pull ..\bv_sc_server split/SCA-5034_AutoApprovalForTcAndSc
git remote remove origin
git remote add origin http://localhost:8090/sc_group/bvpro-job.git
# 新clone的仓库默认是master分支(也可能是main), 然后推送到远端的feature分支
git push --set-upstream origin master:feature/SCA-5034_AutoApprovalForTcAndSc

这样就完成了feature/SCA-5034_AutoApprovalForTcAndSc分支的迁移了,这样迁移也只保留了bvpro-job有关的代码,如果这个分支在以前的其他模块也有代码,那么也要按照这样的方式进行迁移,同时有多少个分支需要迁移就需要执行多次上面的命令。

可以发现这样迁移的效率是很低的,要是分支或者提交记录很多一天的时间都耗在这上面了。当然如果你只想要迁移master分支代码,这种方式也是很不错的,关键是这个命令是git自带的。

git filter-repo

这个命令并不是git自带的,但是它也很赫赫有名,毕竟官方都推荐使用它进行迁移。

使用它是有限制的,首先python版本要在3.5以上,git版本要在2.2以上。因为我使用的是windows,所以下面演示windows上的安装方式

  1. 安装git-filter-repo
    1
    python -m pip install --user git-filter-repo

记录下安装的地址,然后配置环境变量,然后就可以使用git filter-repo这个命令了。

  1. git clone bv_sc_server项目,默认master分支,最好是最新的,这样子命令运行错了也不影响你的开发
1
2
git clone http://localhost:8090/sc_group/bv_sc_server.git bvpro-job

  1. 拆分
1
2
3
4
5
6
7
8
9
10
11
cd bvpro-job

# 只保留bvpro-modules/bvpro-job这个路径的代码
git filter-repo --path bvpro-modules/bvpro-job

# 将bvpro-modules/bvpro-job这个目录提升为根目录
git filter-repo --subdirectory-filter bvpro-modules/bvpro-job

# 上面两句可以合并成下面这句,还可以指定要保留的分支
git filter-repo --path bvpro-modules/bvpro-job --subdirectory-filter bvpro-modules/bvpro-job --branches <保留的分支名称>

上面的命令执行完成后,bv_sc_server这个目录下的代码就只有以前bvpro-job的代码了

  1. 推送
1
2
3
git remote add origin http://localhost:8090/sc_group/bvpro-job.git

git push --set-upstream origin --all

这样子就将bvpro-job这个子模块的所有代码,commit以及分支都迁移到了新的仓库,不用再想subtree一样针对特定的分支反复操作了, 所以我是推荐使用这个命令的。

总结了下这段时间遇到的问题。

快速生成字典表数据

在前期开发的时候,BA总是给我好几张excel,让我生成字典表,写代码又耗时,而且不同的excel字段也不一样,不可能每次都要去改代码吧,总之我不干,好在我能借助excel函数完成这样的需求。

函数

1
2
3
4
5
6
=CONCATENATE("insert into test_claims(`id`,`code`,`name`) values('", A1, "','",B1, "','",C1,"');")

这个函数的语法是 CONCATENATE(text1, [text2], ...)
1. text1(必需):要联接的第一个项目。项目可以是文本值、数字或单元格引用;

2. Text2, ... (可选):要联接的其他文本项目。最多可以有 255 个项目,总共最多支持 8,192 个字符

Kubernetes Pod频繁重启

后台看到部署到kubernetes的pod一直在重启,但是看日志没有报错,但是一会儿它就自动重启了,最后通过describe命令看到是因为liveness接口的原因

1
kubelet  Liveness probe failed: Get "http://10.24.8.84:9202/actuator/health"

因为使用了springboot actuator接口,它会检测服务中使用到的其他服务是否能正常使用,从而判定当前服务是否存活,所以必然是因为这个接口返回的信息导致pod重启。

重启的这个服务主要用到了邮件以及Redis,但是不知道到底是哪个服务健康检查失败了。此时我们也无法进入到pod中访问health接口了。

所以我们先移除掉pod template的livenessProbe配置,然后重新部署服务

1
2
3
4
5
6
7
livenessProbe:
httpGet:
path: /actuator/health
port: 9204
initialDelaySeconds: 60
periodSeconds: 20
timeoutSeconds: 10

这个时候使用exec命令进入pod

1
2

kubectl exec -it <pod name> -n <namespace> -- /bin/bash

访问 curl -i http://localhost:9202/actuator/health 可以看到response status code是503,并且还可以看到具体失败的组件是哪一个。

最终确定是redis访问超时了,于是调整了下 livenessProbe的timeoutSeconds,然后在重启就不报错了。

1
2
3
4
5
6
7
8
9
10
11
12
root@noti-844567c558-2mvj8:/home/pro# curl -i http://localhost:9202/actuator/health
HTTP/1.1 200
X-XSS-Protection: 1; mode=block
Content-Security-Policy: default-src 'self'; script-src 'self'; frame-ancestors 'self'; object-src 'none'
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Content-Type: application/vnd.spring-boot.actuator.v3+json;charset=utf-8
Transfer-Encoding: chunked
Date: Sun, 18 Feb 2024 07:36:22 GMT

{"status":"UP","components":{"discoveryComposite":{"description":"Discovery Client not initialized","status":"UNKNOWN","components":{"discoveryClient":{"description":"Discovery Client not initialized","status":"UNKNOWN"}}},"diskSpace":{"status":"UP","details":{"total":133003395072,"free":94000709632,"threshold":10485760,"exists":true}},"livenessState":{"status":"UP"},"mail":{"status":"UP","details":{"location":"xxmail:465"}},"ping":{"status":"UP"},"readinessState":{"status":"UP"},"redis":{"status":"UP","details":{"version":"6.0.14"}},"refreshScope":{"status":"UP"}},"groups":["liveness","readiness"]}

minio数据迁移到Azure blob

项目中需要将minio中保存的文件迁移到azure的blob中,然后minio中保存文件的路径是这样的 2023/11/07/20231107164256A579/Image3.jpg 相当于文件夹中包含了时间戳等信息,虽然blob不支持目录,但是它的虚拟目录可以有相同的效果,这里我们使用azure提供的azcopy命令来进行数据迁移。

首先进入到minio所在的服务器,然后执行下面的命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

1. sudo mkdir /home/azcopy

2. cd /home/azcopy

3. sudo wget -O azcopy_v10.tar.gz https://aka.ms/downloadazcopy-v10-linux &&

4. sudo tar -xf azcopy_v10.tar.gz --strip-components=1

5. sudo azcopy login // 登录azure


6. 同步minio数据到blob,将sc-dev bucket的数据迁移到blob的container


// 这里的sc-dev是minio的bucket, saoscdev是azure blob的account name, bvsc是container name
a. sudo /home/azcopy/azcopy copy '/data/minio/data/sc-dev/*' 'https://saoscdev.blob.core.windows.net/bvsc' --recursive

// 将增量数据同步到blob中
b. sudo /home/azcopy/azcopy sync '/data/minio/data/sc-dev' 'https://saoscdev.blob.core.windows.net/bvsc' --recursive

总结

以上就是我最近遇到的问题,第一个字典表的那个当时第一反应是写代码,但是后来想到写代码的时候太长,成本太高还是需要借助工具,恰好提供给我的又是excel,于是就用excel顺带完成了这个功能,同时后面的其他字典数据如法炮制,也就变简单了。

第二个问题本来是定位重启的问题,但是看着看着就深入到了actuator的源码当中起了,这一点很不好,不过有失必有得,有顺带看了下actuator的源码,它其中的EntryPoint感觉很棒,后面针对这个写一篇。

第三个问题的话就是微软没有提供如何迁移的文档,不过好在它本生工具不少,多尝试也就成功了,这里我把迁移的步骤给出来,希望能帮助到同样有需求的人。

报错

最近项目中访问一个外部api报错了,报错信息如下

1
PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

看着像是证书问题,这个时候我首先想到的是百度下,看看怎么解决。

解决方案

百度告诉我说如果你open-feign中使用的是http client,那么可以通过下面的配置来让跳过SSL验证

1
2
3
feign:
httpclient:
disable-ssl-validation: false

结果还是报同样的错误。 于是我又百度,又重新找了一个解决方法,这次的方案是让我自己重写Client了,具体操作如下

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
@Configuration
public class FeignConfiguration {

@Bean
public Client feignClient() throws NoSuchAlgorithmException, KeyManagementException {
SSLContext ctx = SSLContext.getInstance("SSL");
X509TrustManager tm = new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
};
ctx.init(null, new TrustManager[]{tm}, null);


return new Client.Default(ctx.getSocketFactory(), (hostName, session) -> true);

}

}

这把我感觉要起飞了, 一切尽在掌握中,重新deploy,打开postman,测试测试我的接口。

测试后感觉好了但是看日志又没有完全好。 这个接口倒是不报错了,但是我调用内部服务给我报错了,比如我这里的内部服务名称叫做

pro-file, 就现在它没法根据我这个pro-file名字找到对应的IP了,从而导致我这个服务使用不了了。

百度误我!

求人不如求己

此刻我自信的打开了IDEA, 输入了类名 FeignAutoConfiguration , Spring Cloud关于某个组件的自动注入类大多是XXXConfiguration, 所以按照这么找准没错。

然后我有自信的把断点打在了这个部分 FeignAutoConfiguration:246

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ApacheHttpClient.class)
@ConditionalOnMissingBean(CloseableHttpClient.class)
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
@Conditional(HttpClient5DisabledConditions.class)
protected static class HttpClientFeignConfiguration {
// 省略其他代码

@Bean
@ConditionalOnMissingBean(Client.class)
public Client feignClient(HttpClient httpClient) {
return new ApacheHttpClient(httpClient);
}

}

重新启动项目,好家伙断点没进来呀。 没进来的原因大概率可能是不满足条件,我赶紧看看这里对应的Conditional, 发现了我的代码中没有

设置feign.httpclient.enabled属性的值, 而且这里也没有设置havingValue, 根据源码可以知道, 如果没有设置havingValue, 那么这个属性的值会被和false进行比较

1
2
3
4
5
6
7
8
9
//org.springframework.boot.autoconfigure.condition.OnPropertyCondition.Spec#isMatch
// 这里的requiredValue是havingValue
private boolean isMatch(String value, String requiredValue) {
if (StringUtils.hasLength(requiredValue)) {
return requiredValue.equalsIgnoreCase(value);
}
return !"false".equalsIgnoreCase(value);
}

搞半天这个Configurtion相当于没起作用。

好好好,这么玩是吧。

既然这个配置不生效,那肯定有其他配置生效,我就找找其他配置,最终我在spring-cloud-openfeign-core这个jar包的loadbalancer这个包下面找到了我想要的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@ConditionalOnClass(Feign.class)
@ConditionalOnBean({ LoadBalancerClient.class, LoadBalancerClientFactory.class })
@AutoConfigureBefore(FeignAutoConfiguration.class)
@AutoConfigureAfter({ BlockingLoadBalancerClientAutoConfiguration.class, LoadBalancerAutoConfiguration.class })
@EnableConfigurationProperties(FeignHttpClientProperties.class)
@Configuration(proxyBeanMethods = false)
// Order is important here, last should be the default, first should be optional
// see
// https://github.com/spring-cloud/spring-cloud-netflix/issues/2086#issuecomment-316281653
@Import({ HttpClientFeignLoadBalancerConfiguration.class, OkHttpFeignLoadBalancerConfiguration.class,
HttpClient5FeignLoadBalancerConfiguration.class, DefaultFeignLoadBalancerConfiguration.class })
public class FeignLoadBalancerAutoConfiguration {

}

因为我们项目是采用springcloud alibaba进行开发,所以引入了spring-cloud-loadbalancer这个包,因此这个这个配置类就会生效,由于我们没有配置使用httpclient,同样也未使用okhttp,所以生效的配置类只有一个,那就是 DefaultFeignLoadBalancerConfiguration

这个配置类中retryClient会被加载,因为我们引入了spring-retry.

1
2
3
4
5
6
7
8
9
10
11
@Bean
@ConditionalOnMissingBean
@ConditionalOnClass(name = "org.springframework.retry.support.RetryTemplate")
@ConditionalOnBean(LoadBalancedRetryFactory.class)
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled", havingValue = "true",
matchIfMissing = true)
public Client feignRetryClient(LoadBalancerClient loadBalancerClient,
LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerClientFactory loadBalancerClientFactory) {
return new RetryableFeignBlockingLoadBalancerClient(new Client.Default(null, null), loadBalancerClient,
loadBalancedRetryFactory, loadBalancerClientFactory);
}

这也是为什么上面我们自己配置了自己的Client后,访问其他spring cloud服务会找不到地址,这是因为默认的client不会去通过LoadBalancer去获取服务地址。

小插曲

期间debug的时候,还发现最终的Client的SeataFeignClient,我一看才发现某个公共包引入了Seata,但是没有使用Seata功能,然后Seata会把我们最终使用的FeignClient在给封装一次,所以后面我就把seata从项目中移除了。

解决方案

既然问题找到了,那么就好修改了,修改方式有两种,一种是创建自己的RetryableFeignBlockingLoadBalancerClient, 就把上面的代码拿过来抄一遍,只是自己指定SSLContext,另一种是启用httpclient

方案一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Bean
public Client feignRetryClient(LoadBalancerClient loadBalancerClient,
LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerClientFactory loadBalancerClientFactory) throws NoSuchAlgorithmException, KeyManagementException {
SSLContext ctx = SSLContext.getInstance("SSL");
X509TrustManager tm = new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
};
ctx.init(null, new TrustManager[]{tm}, null);


return new RetryableFeignBlockingLoadBalancerClient(new Client.Default(ctx.getSocketFactory(), (hostname, session) -> true), loadBalancerClient,
loadBalancedRetryFactory, loadBalancerClientFactory);
}

方案二

另一种方案就是启用httpclient,并且禁用ssl验证,配置如下

1
2
3
4
5
feign:
httpclient:
enabled: true
disable-ssl-validation: true

自此这个问题解决了,当然在使用中更加倾向使用方案二,因为Feign默认的Client采用的是HttpURLConnection,它没有连接池,当然你也可以使用okhttp。

写到最后

这个问题看起来简单,但是排查起来还是颇费心思,很多细节隐藏到了框架之下,所以我想看源码还是有好处的,因为网上的文章别人的情况可能和你不一样,与其遨游在各个文章里面,还不如debug一把。

之前文章<老大喊我7天从SpringCloud转到K8S>中我提到过我们使用open-feign来访问内部服务,有很多童鞋留言问我访问内部服务的url应该怎么写,但是还是整个系统的整体url吗?

比如我整个系统的url是 http://www.think123.com, 现在有user服务和manage服务,那我现在想要访问user服务的ip就变成 http://www.think123.com/user吗? 当然不是因为这样的话你的访问就相当于还是访问外网,又要重新走gateway, 这样子不仅增加网络开销,而且我们做鉴权也不方便,因为服务间的访问鉴权往往是通过一个注解或者token来实现的。

<!-more–>

服务的ip地址

我们都知道在k8s中我们一般会部署多个pod, 而我们又是通过K8S的Service来做LoadBalance的,Service会根据一定的策略来选择具体访问的pod, 如下图所示

访问

因此实际上我们只需要知道service的地址就行了,使用下面的命令查看service的地址

1
kubectl get  svc -n <namespace>

service

我们可以看到service的ip,因为我们服务是部署到同一个集群的,那是不是只需要知道服务的ip就行了,然后在open-feign中声明要访问的服务的ip加上端口就可以了呢?

原则是上可以的,但是实际上有问题,首先不同集群中ip地址不一样,如果你要部署不同集群,那么每次都要修改,其次就算只有一个集群,这个ip也是可能变动的,所以写ip不太靠谱。

服务的域名

好在kubernetes给某个服务都添加了一个域名(这是通过kube-proxy和iptables实现的), 域名的规则是 ..svc.cluster.local

比如对于我们的user服务,我们这个地址就是 user.dev.svc.cluster.local(<serviceName>.<namespace>.svc.cluster.local)

当然后面的这么一堆你都可以省略,实际上我们在open-feign的url中只需要声明 http://user:9201/(服务名:端口) 就可以了,kubernetes在进行dns寻址的时候会先在本地dns找user这个域名对应的ip地址

K8S的DNS解析机制

  1. 集群域名和后缀:Kubernetes集群中的每个服务都会被分配一个域名,该域名由服务名称(Service Name)和命名空间(Namespace)组成。例如,一个服务名为my-service,位于命名空间my-namespace的服务的完整域名将是my-service.my-namespace.svc.cluster.local。svc.cluster.local是Kubernetes集群默认的后缀。

  2. Kubernetes DNS服务器:Kubernetes集群内部有一个专用的DNS服务器负责处理服务的DNS解析请求。这个DNS服务器通常被命名为kube-dns或coredns。

  3. 解析流程:在进行DNS解析时,应用程序或服务可以使用服务名作为主机名(hostname),然后发送DNS查询请求到Kubernetes DNS服务器。

  4. DNS查询:Kubernetes DNS服务器接收到DNS查询请求后,会根据请求中的域名信息进行解析。它首先进行域名拆分,将服务名、命名空间和集群后缀分离开。

  5. 域名解析:Kubernetes DNS服务器会依次解析域名的各个部分。它首先解析命名空间,然后根据服务名在该命名空间下查找对应的Service资源。

  6. Service资源解析:Kubernetes DNS服务器在Service资源中查找与请求的服务名和命名空间匹配的条目。如果找到匹配项,将返回与之关联的Pod IP地址列表。

  7. IP地址返回:Kubernetes DNS服务器将解析到的Pod IP地址返回给发起请求的应用程序或服务。

  8. 重试机制:如果在初始查询时没有找到匹配的Service资源,Kubernetes DNS服务器可能会进行一些重试机制,以确保服务名得到正确解析。这样做是因为在创建和删除Service资源的过程中,可能会存在一定的延迟。
    通过这种方式,Kubernetes DNS解析机制使得服务能够通过服务名进行通信,无需关心具体的Pod IP地址。这种抽象层简化了服务之间的通信配置,并支持动态扩展和管理服务。

查看域名

可以通过下面的命令查看域名

1
kubectl get svc my-service -n my-namespace -o jsonpath='{.metadata.name}.{.metadata.namespace}.svc.cluster.local'

查看coredns

通过下面的命令可以查看coredns pod

1
kubectl -n kube-system get pods -l k8s-app=kube-dns

当然我们可以通过下面的命令查看coredns的配置

1
2
kubectl -n kube-system get cm -l k8s-app=kube-dns
kubectl describe cm coredns -n kube-system

其核心配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.:53 {
errors
ready
health
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
}
prometheus :9153
forward . /etc/resolv.conf
cache 30
loop
reload
loadbalance
import custom/*.override
}


  1. .:53 表示监听的 DNS 端口号为 53,. 表示根域名(Root Zone)。

  2. kubernetes cluster.local in-addr.arpa ip6.arpa 定义了多个域和反向解析配置项。

    • kubernetes 是 Kubernetes 插件的名称,用于解析 Kubernetes 集群的服务和 Pod。

    • cluster.local 是 Kubernetes 集群内部域名的默认后缀。

    • in-addr.arpaip6.arpa 是用于反向 DNS 解析的 IPv4 和 IPv6 地址后缀。

    • pods insecure 允许对 Pod 进行非安全(insecure)的 DNS 解析。

    • fallthrough in-addr.arpa ip6.arpa 表示如果查询未匹配到任何资源记录,则继续向下查询反向 DNS 解析。

  3. forward . /etc/resolv.conf 将未能解析的 DNS 请求转发给 /etc/resolv.conf 文件中配置的其他 DNS 服务器。

大家也可以去看pod中中/etc/host和/etc/resolv.conf然后也能发现一些迹象。

结束语

至此,讲清楚了集群中内部服务之间是如何访问的,且k8s是如何进行寻址导致对应的pod的,希望对大家有所帮助。

最近一个新的项目中的一个业务,状态的流转比较复杂,涉及到二十几个状态的流转,而且吸取了其他业务教训,我们决定使用状态机来解决状态流转的问题。

要使用状态机除了自己写状态模式下还研究了当下两个开源项目,一个是spring的state machine,一个是cola-state-machine。

spring的状态机可以做状态持久化,和spring结合比较好,但是太重了。 cola就比较简单,它只是简单做了一个抽象,我们只需要实现具体的行为就行了。 使用cola最重要的就是要记得”因为某个事件,导致了状态A向状态B进行了迁移”,当然这里的状态可以是同一个。

因为项目中使用的是springboot,所以我这里结合起来做了一定的改造,下面给出我在项目中使用的例子,仅供大家参考

引入依赖

1
2
3
4
5
6
<dependency>
<groupId>com.alibaba.cola</groupId>
<artifactId>cola-component-statemachine</artifactId>
<version>4.3.2</version>
</dependency>

定义

因为某个事件,导致了状态A向状态B进行了迁移。所以需要定义状态,事件,流程。

状态迁移

事件

根据我们的流程我定义了以下事件

1
2
3
4
5
6
7
8
9
10
11
@AllArgsConstructor
@Getter
public enum StatusChangeEventEnum {
// save as draft
SAVE_AS_DRAFT_EVENT,
// draft submit
DRAFT_SUBMIT_EVENT,
// submit
SUBMIT_EVENT;
}

状态

1
2
3
4
5
6
7
8
9
10
11
12
@Getter
@AllArgsConstructor
public enum StatusEnum {

NONE(0, "None"),
DRAFT(1, "Draft"),
SUBMITTED(2, "Submitted");

private Integer code;
private String desc;
}

流程

定义状态迁移和事件的关系

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
@Getter
@AllArgsConstructor
public enum StatusChangeEnum implements StatusChange {

SAVE_AS_DRAFT(NONE, DRAFT, SAVE_AS_DRAFT_EVENT),
SUBMIT(NONE, SUBMITTED, SUBMIT_EVENT),
DRAFT_SUBMIT(DRAFT, SUBMITTED, DRAFT_SUBMIT_EVENT);

private StatusEnum fromStatus;
private StatusEnum toStatus;
private StatusChangeEventEnum event;

@Override
public StatusEnum from() {
return fromStatus;
}

@Override
public StatusEnum to() {
return toStatus;
}

@Override
public StatusChangeEventEnum event() {
return event;
}

}

// 抽象状态变更的接口,因为可能会存在多个不同的状态变更流程
public interface StatusChange {

StatusEnum from();

StatusEnum to();

StatusChangeEventEnum event();

}

使用Spring管理状态机

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
46
47
48
49
50
51
52

@Configuration
@RequiredArgsConstructor
public class StateMachineConfiguration {
private final List<StateMachineHandler> handlerList;
private static final String NEW_REQUEST_STATE_MACHINE = "newRequestStateMachine";

@Bean(NEW_REQUEST_STATE_MACHINE)
public <C> StateMachine<StatusEnum, StatusChangeEventEnum, C> newRequestStateMachine() {

StateMachineBuilder<StatusEnum, StatusChangeEventEnum, C> builder = StateMachineBuilderFactory.create();

for (StatusChangeEnum changeEnum :StatusChangeEnum.values()) {
build(builder, changeEnum);
}
return builder.build(NEW_REQUEST_STATE_MACHINE);
}

private <C> void build(StateMachineBuilder<StatusEnum, StatusChangeEventEnum, C> builder, StatusChange statusChange) {
// 找到对应的handler来处理
StateMachineHandler<StatusEnum, StatusChangeEventEnum, C> handler = getHandler(statusChange);

StatusEnum fromStatus = statusChange.from();
StatusEnum toStatus = statusChange.to();
StatusChangeEventEnum changeEvent = statusChange.event();
// 只产生了事件,但是状态未发生变化
if (fromStatus == toStatus) {
builder.internalTransition()
.within(fromStatus)
.on(changeEvent)
.when(handler::isSatisfied)
.perform((from, to, event, ctx) -> handler.execute(from, to, event, ctx));
} else {
builder.externalTransition()
.from(fromStatus)
.to(toStatus)
.on(changeEvent)
.when(handler::isSatisfied)
.perform((from, to, event, ctx) -> handler.execute(from, to, event, ctx));
}
// 直接在handler中抛出更详细异常
//builder.setFailCallback(new AlertFailCallback<>());
}

private <C> StateMachineHandler<StatusEnum, StatusChangeEventEnum, C> getHandler(StatusChange statusChange) {
return handlerList.stream().filter(handler -> handler.canHandle(statusChange))
.findFirst()
.orElse(new DefaultStateMachineHandler<C>());
}

}

经过上面的定义后,后续有新的状态变更流程,我们只需要在 StatusChangeEnum 中添加就行了。

实现对应的handler

这里我举一个例子,比如说首次提交数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

@Component
@RequiredArgsConstructor
public class NoneToSubmittedStatusHandler implements
StateMachineHandler<StatusEnum, StatusChangeEventEnum, SubmitDTO> {
@Override
public boolean canHandle(StatusChange statusChange) {
// handler能处理的变更流程
StatusChangeEnum changeEnum = StatusChangeEnum.SUBMIT;
return statusChange == changeEnum;
}

@Override
public void execute(StatusEnum from, StatusEnum to, StatusChangeEventEnum event, SubmitDTO context) {
// 执行具体的业务逻辑,比如插入数据
}

@Override
public boolean isSatisfied(SubmitDTO context) {
// 判断是否满足条件,比如是否是对应的用户等
return true;
}
}

这样子我们的每一个handler的功能就比较专一了,只需要处理对应状态的就行了,你可能回想要是有些状态的变成要做的事情类似,这样的代码不可能写两遍吧? 其实我们可以有一个抽象类可以将这些公用的逻辑放到抽象类里面,这样子有相同逻辑的就可以使用了。

使用

万事具备,现在只差在项目中使用了

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
@Component
@RequiredArgsConstructor
public class StateMachine {

private final StateMachine<StatusEnum, StatusChangeEventEnum, StatusChangeContext> newRequestStateMachine;


@Transactional(rollbackFor = Exception.class)
public void statusChange(StatusChange changeEnum, StatusChangeContext context) {
newRequestStateMachine.fireEvent(changeEnum.from(), changeEnum.event(), context);
}

}

public abstract class StatusChangeContext {

}

@Data
public class SubmitDTO extends StatusChangeContext {

private Long id;

private Long String;
}

只要涉及到状态变更的,就都可以调用StateMachie了。

写到最后

这种方式其实会导致类的数量变多,但是职责更加清晰,每个类的代码行数也并不多,而且以后想要找某个状态变更到某个状态做了什么时候很很好找。

这就是最近使用状态机的一些心得,希望能对你有所帮助。

helm官方文档

hi, bro, 最好的永远是官方文档: https://helm.sh/zh/docs/

就是它上面的东西太多了,我知道你看得费力,所以我给你总结了下面的常用命令

helm添加仓库

1
2
3
4
5
6
7
8
9
10
11
helm repo add  elastic  https://helm.elastic.co       
helm repo add gitlab https://charts.gitlab.io
helm repo add harbor https://helm.goharbor.io
helm repo add traefik https://traefik.github.io/charts

//添加国内仓库
helm repo add stable http://mirror.azure.cn/kubernetes/charts
helm repo add aliyun https://kubernetes.oss-cn-hangzhou.aliyuncs.com/charts
helm repo update
helm repo list

阅读全文 »

之前项目使用的是springcloud,主要使用到的组件有 spring gateway, nacos, minio, load balancer,open-feign等,然后我们的微服务通过docker部署到虚拟机里面的。

但是出于安全的考虑,需要将它迁移到azure的aks(kubernetes)中,所以需要将spring cloud改造成spring boot。这样就不用自己维护虚拟机的安全策略,也不需要去关注补丁了。

梳理项目结构

项目是一个一个微服务组织起来的,大概业务类的服务有5个,公共服务有4个。 设计到的改造主要集中在gateway, auth中,公共包的一些改造比较少,主要是将open-feign的访问改为通过url进行调用,而不是之前通过服务名来。

而在kubernetes中,我们使用Traefik2来代替gateway的功能,不知道traefik2的,可以去翻翻之前的文章。

同时对于授权,需要提供一个授权接口,配合traefik2使用,这样每一个请求都会进行授权的验证。

开始改造

确定分支

最开始肯定是新拉一个分支进行这些改动,即便没改好也不影响其他人,所以我们先把分支名定好就叫做 feature/AKS-migrate。

改造gateway

首先把pom文件中的不需要的依赖包注释掉,比如spring cloud gateay, nacos, sentinel等spring cloud相关组件。注释掉之后
去看代码中有哪些报错,就针对性的修改。

我们的项目中使用蛮多gateway的filter以及handler,最开始我看他们都是使用的webflux,我就想我单独引入这个包,代码是不是最小的改动就行了呢?

这样尝试过之后我发现不行,因为项目中使用最多还是@RestController,如果使用webflux的方式,那么很多filter不生效的。

所以这种方式也不行,但是代码改得太多了,我只好回退到注释pom文件依赖的那一步。

没办法,只能读之前对应的代码逻辑,然后将其转换了。

读取gateway filter的代码,将其转换成spring filter,直接继承 org.springframework.web.filter.OncePerRequestFilter 即可,然后将之前的逻辑搬过来。

需要注意的是如果是全局filter需要放到公共包里面。

handler也是一样的,将其转换成filter,需要注意执行顺序。

这样,核心代码改造完毕,可以开启调试了

遇到的坑

处理上面说的webflux的问题外,将springcloud变成springboot后,我们之前的配置文件名称是bootstrap.yml,bootstrap-dev.yml文件,但是改成了springboot后,配置文件名要改成application.yml,application-env.yml。

不然你会发现你启动不了,说找不到文件,这个坑也是自己把自己给坑了。

然后就是要将gateway的filter转换成spring filter的时候要注意一定要保证之前的逻辑完全移植过来了,我就遇到一个在改造前可以重复读取request的流,但是改造后这段代码报错了,就是因为没有将这段逻辑移植过去的问题。

改造nacos

前面提到了nacos主要在open-feign的调用中以及变量注入中使用到了。feign那个好改,只需要指定url参数即可,这样就可以去掉nacos的依赖了。 然后变量注入同样的我们可以使用Kubernetes的ConfigMap以及Secret来代替。

所以我们需要将以前配置到nacos中的变量放到配置文件中,这样变量可以直接通过Kubernetes进行注入了。

我们在各个环境中只需要有一份代码(一个镜像),部署的时候只需要注入的配置不一样就可以了,这样就可以保证各个环境代码一致。

比如之前的配置是这样的

1
2
3
4
5
6
7
spring:
redis:
host: 127.0.0.1
port: 6379
database: 0
password: 123456

改造后配置文件的值为

1
2
3
4
5
6
spring:
redis:
host: ${env_redis_host}
port: ${env_redis_port}
database: ${env_redis_database}
password: ${env_redis_password}

而这里的变量是通过ConfigMap进行配置的,到时候会注入到容器环境变量中,这样spring就可以从环境变量中获取到值了。

部署

之前使用的jenkins方式部署,jenkins也是自己搭建的,现在全部迁移到了azure github上,所以这里直接使用azure的pipeline进行部署。 而我们管理k8s的资源则使用的是helm。

比如我项目中使用helm生成后结构如下

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
C:.                 
│ .helmignore
│ Chart.yaml
│ values-prod.yaml
│ values-qa.yaml
│ values-test.yaml
│ values.yaml

├─charts
├─config
│ ├─dev
│ │ config.yaml
│ │ secret.yaml
│ │
│ ├─prod
│ │ config.yaml
│ │ secret.yaml
│ │
│ ├─qa
│ │ config.yaml
│ │ secret.yaml
│ │
│ └─test
│ config.yaml
│ secret.yaml

└─templates
configmap.yaml
deployment.yaml
hpa.yaml
secret.yaml
service.yaml
_helpers.tpl

这里只需要部署的时候指定不同的value 文件,就可以实现同一个镜像部署到不同的环境了。

dev目录下config.yaml,secret.yaml文件内容大致如下:

1
2
3
4
5
6
7
8
9
10
# config.yaml
env_redis_host: localhost
env_redis_port: 6379
env_redis_database: 1


#secret.yaml

env_redis_password: 123456

在template中configmap.yaml, secret.yaml中主要是如何将文件内容转换成对应的yaml

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
#values.yaml 指定有哪些文件
configOverrides:
- config/dev/config.yaml
secretOverrides:
- config/dev/secret.yaml



# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "think-manifesto.fullname" . }}-configmap
namespace: {{ .Values.nameSpace }}
data:
{{- $files := .Files }}
{{- range .Values.configOverrides }}
{{- range $key, $value := ($files.Get (printf "%s" .) | fromYaml) }}
{{ $key | indent 2 }}: {{ $value | quote }}
{{- end }}
{{- end }}

# secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: {{ include "think-manifesto.fullname" . }}-secret
namespace: {{ .Values.nameSpace }}
type: Opaque
data:
{{- $files := .Files }}
{{- range .Values.secretOverrides }}
{{- range $key, $value := ($files.Get (printf "%s" .) | fromYaml) }}
{{ $key | indent 2 }}: {{ $value | b64enc }}
{{- end }}
{{- end }}


最后给我deployment.yaml的例子,大家可以参考下

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "think-manifesto.fullname" . }}
namespace: {{ .Values.nameSpace }}
labels:
{{- include "think-manifesto.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "think-manifesto.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "think-manifesto.selectorLabels" . | nindent 8 }}
annotations:
{{- if .Values.configOverrides}}
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
{{- end }}
{{- if .Values.secretOverrides}}
checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
{{- end }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: {{ .Values.service.portName }}
containerPort: {{ .Values.service.port }}
envFrom:
{{- if .Values.configOverrides }}
- configMapRef:
name: {{ include "think-manifesto.fullname" . }}-configmap
{{- end }}
{{- if .Values.secretOverrides }}
- secretRef:
name: {{ include "think-manifesto.fullname" . }}-secret
{{- end }}
{{- with .Values.livenessProbe }}
livenessProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.readinessProbe }}
readinessProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

至于values.yaml我就不给出来了,基本上是其他模板需要什么就写在上面就好了。

和Kustomize相比,helm安装第三方chart很方便,它有自己的仓库,这里附上我安装traefik2的命令

1
2
3
4
5
6
7
8
9
10
# 添加traefiK仓库
helm repo add traefik https://traefik.github.io/charts
#添加国内仓库
helm repo add stable http://mirror.azure.cn/kubernetes/charts
helm repo add aliyun https://kubernetes.oss-cn-hangzhou.aliyuncs.com/charts
helm repo update
helm repo list

helm install --set deployment.kind=DaemonSet --set namespaceOverride=traefik --set service.enabled=false traefik traefik/traefik

本地验证

当你写好了上面的chart之后,如果你本地没有kubernetes环境(因为它可能在服务器才存在),而你又想要在本地进行验证你写得是否有问题,那么可以使用下面的命令。

1
2
// 将下面的变量替换成你自己的。 chart-name表示chart的名字,chart-dir表示chart地址
helm template --dry-run --debug --disable-openapi-validation ${chart-name} .\${chart-dir}\

然后如果你想要在k8s环境中安装的时候,而k8s环境又在远端服务器,那么你可以将chart打包,然后到服务器中进行安装,然后也可以在
将chart上传到服务器中,然后进行安装(服务器中要先安装helm)。

写到最后

自此,从springcloud迁移到k8s集群总算是完成了。因为是第一次使用helm(以前都用的kustomize),所以在helm这里耗费了一些功夫,主要是排查错误方面的,不过不得不说helm的文档写得不错,很清晰。

再然后就是代码改造以及一些配置问题,因为迁移azure,所以上面的关于它pipeline的一些配置不是很清楚,不过好在可以直接练习他们的运维,还是帮我们解决了一些问题的。