Galaxy

姚皓的技术博客-一杯咖啡,一首音乐,一台电脑,编程

0%

微服务的开发模式下,联调和服务注册一旦涉及多个环境(开发/SIT/UAT),就会变得有些复杂。本文总结一下我们在此问题上尝试过的几个workaround,以及最终推荐的方案。

1. 背景

以下描述的案例中,将我们所拥有的服务精简为三个:

  • um:用户微服务
  • ent:企业微服务
  • bi:BI微服务
    ent会调用um;bi会调用ent和um。
    网络环境分成办公网段和开发环境网段。办公网段可以访问开发环境网段,但开发环境网段无法访问办公网段。
    三个微服务都被打包成镜像,以单副本Pod的形式部署在K8S云的开发环境节点上。
    服务注册使用Nacos,网关路由使用的是Zuul。

部署环境

2. 单环境内部请求流程

如果只考虑SIT环境,整个服务注册+请求的处理流程可以简单描述如下:

  1. um-sit服务(um的sit环境,下同)启动,将自己的service ip注册到Nacos服务端
  2. ent-sit服务启动,将自己的service ip注册到Nacos服务端
  3. 前端web对http://域名/api/ent-sit 的某个接口发起请求
  4. 通过K8S Ingress的域名映射,找到了Zuul应用
  5. Zuul向Nacos查询ent-sit的地址,得到ip:172.0.0.2。这个是ent-sit的service内部ip
  6. Zuul将请求转给ent-sit的service,Pod里的ent-sit容器中的应用接收到请求,开始处理
  7. ent-sit容器在处理过程中需要解析token,于是向Zuul请求um-sit
  8. Zuul向Nacos查询um-sit的地址,得到ip:172.0.0.1。这个是um-sit的service内部ip
  9. Zuul将请求转给um-sit。um处理完token,返回用户信息
  10. ent-sit处理结束,将结果返回给Zuul
  11. Zuul将结果转给前端web,流程结束
阅读全文 »

从Oracle或MySQL切换到PostgreSQL(以下简称pgsql)后,多少有些不一样的地方需要适应。这里就将和开发相关的一些区别挂一漏万地列举一下。

1. Schema模式

和Oracle与MySQL一样,pgsql中也有TableSpace(表空间),用于定义用来存放表示数据库对象的文件的位置。
但在Schema(模式)的定义上,三者有很大的差别。
对于MySQL,模式与数据库同义。甚至可以用CREATE SCHEMA来创建数据库,效果和CREATE DATABASE一样。
对于Oracle,schema与数据库用户密切相关:

A schema is a collection of logical structures of data, or schema objects. A schema is owned by a database user and has the same name as that user. Each user owns a single schema.

而pgsql中,层次结果如下:
PostgreSQL Hierachy

从图中可以看到,schema是database与table中间的一层。可以理解为命名空间类似的概念。当新创建一个数据库时,pgsql会默认创建一个public schema。如果没有指定的话,就是以public schema来操作各种数据对象。 例如:CREATE TABLE products ( ... ) 等同于 CREATE TABLE public.products ( ... )
schema不能互相嵌套。同一个schema下不能有重复的对象名字,但在不同schema下可以重复。
schema与database的差别在于schema不是严格分离的:一个用户可以访问他所连接的数据库中的任意模式中的对象。

对于数据库管理人员来说,还需要了解一下授权相关的差别,但在本文中就略过了。更多可以参考这篇:PostgreSQL · 特性分析 · 逻辑结构和权限体系

1.1 Schema与开发相关

阅读全文 »

搜了一下中文技术博客上似乎没有相关的文章,就简要翻译一下。

需求

假设公司内部有非常多Maven项目,需要deploy到一个内部maven私有仓库中。
如果希望maven deploy命令可以成功执行,一般需要在pom.xml中添加:

1
2
3
4
5
6
<distributionManagement>
<repository>
<id>nexus-site</id>
<url>http://central_nexus/server</url>
</repository>
</distributionManagement>

但需要deploy的项目很多的情况下,我们肯定不希望在每个项目的pom文件中都重复添加这个配置。

方案一

为所有项目增加一个公共的parent pom项目。那么只需要在这个项目的pom文件中添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>your.company</groupId>
<artifactId>company-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>

<distributionManagement>
<repository>
<id>nexus-site</id>
<url>http://central_nexus/server</url>
</repository>
</distributionManagement>

</project>
阅读全文 »

本篇主要针对的是Jenkins服务器处在局域网中,无法连上互联网的情况下如何做自动化部署。
本文对网络的架设是:有内部的git服务器,yum私有仓库,nexus Repository OSS私有仓库和npm私有仓库。如果这些条件都没有,可能你们还是本地开发机上打包稍微快一些。

1. 安装

1.1 安装必要依赖

Jenkins的必要依赖是JDK。后续自动化部署的必要依赖是git,Maven和Node.js。
具体的安装步骤就不详述了。rpm安装(针对JDK)或私有YUM仓库安装都可以。
例如私有YUM仓库中openjdk,直接运行yum install java-1.8.0-openjdk.x86_64 即可。

Maven私有仓库配置
Maven打包的时候默认会从公网的仓库拉取依赖的第三方库。我们需要将其改为指向私有仓库。
首先可以通过如下两条命令之一获得配置文件地址:

1
2
mvn --version
mvn -e -X

假设settings.xml文件的位置在/etc/maven/路径下。
编辑该文件内容:

1
vi /etc/maven/settings.xml
阅读全文 »

起因

最近在整理代码规范,按照之前oracle的习惯,定了以下的字段长度设定规范:

  • 名称字段:varchar(200)
  • 较长的名称字段/简介字段:varchar(500)
  • 特别长的描述字段: varchar(2000)
  • 超过2000中文字的字段:text
    为什么是200长度,而不是100或300,也是拍脑袋想的,类似DND里的房规。
    但在被问起为什么不设置为经常见到的varchar(255)时,一时回答不上来。趁这个机会,把字段长度这块的知识汇总梳理一下。

为什么会经常被设置为varchar(255)

MySQL 4.1版本之前,varchar的最大长度是255 byte字节(也有一说是5.0.3版本之前)。查了下这个版本发布都是2004年的事情了。惯性真恐怖,我可不相信还有多少系统是从2004年升级过来的。

varchar(50)和varchar(255)有性能上的差别么?

对于INNODB,varchar(50)varchar(255)这两者在存放方式上完全一样:1-2 byte保存长度,实际的字符串存放在另外的位置,每个字符1 byte到4 byte不定(视编码和实际存储的字符而定)。所以将一个字段从varchar(50)长度改成varchar(100)长度不会导致表的重建。但如果把长度从varchar(50)改成varchar(256)就不一样了,表示长度会需要用到2 byte或更多。

既然255长度以下对INNODB都一样,而且我们平时基本上也不太会使用到MYISAM,那么是不是为了省心,我们就可以把255长度以下的字段的类型都设置成varchar(255)了呢?
非也。
因为内存表介意。
虽然我们不会明文创建内存表,但所有的中间结果都会被数据库引擎存放在内存表。我们可以通过EXPLAIN或者SHOW STATUS可以查看MYSQL是否使用了内存表用来帮助完成某个操作。
而内存表会按照固定长度来保存。以utf-8编码为例,对于varchar(255),每一行所占用的内存就是长度的2 byte + 3 * 255 byte。对于100条数据,光一个varchar字段就占约1GB内存。如果我们该用varchar(50),就可以剩下来约80%的内存空间。
除此之外,255长度也可能会对索引造成坑。MySQL在5.6版本及之前的最大长度是767 byte。但MySQL 5.5版本后开始支持4个byte的字符集utf8mb4(沙雕表情用到的字符太多,长度不够用)。255 * 4 > 767,所以索引就放不下varchar(255)长度的字段了。虽然MySQL在5.7版本后将限制改成了3072 byte,但如果是多字段的联合索引还是有可能会超过这个限制。

阅读全文 »

有部分老Web系统只有在IE下才能正常打开。其中有一部分是即使polyfill也没法搞定的兼容性原因,另一部分就是因为使用到了ActiveX。后者中我接触到的就有金格控件和泛微OA的。
对于新开发的Portal系统,没有余力为了迁就IE,对每个功能还额外做兼容性测试。于是剩下的方案就是在单点登录跳转到相应的页面的时候,指定使用IE打开。
其实这个功能并不罕见。比如腾讯的网站上经常有点击图标打开QQ,而淘宝网页上也有很多点击打开阿里旺旺。从原理上,这是利用到了Windows自定义协议URI Scheme。

URI Scheme

自定义协议从本质上就是修改注册表。官方资料可以参考这篇Registering an Application to a URI Scheme (Windows) | Microsoft Docs
官方给了一个范例,注册一个alert://的协议,点击后打开自定义的alert.exe。

1
2
3
4
5
6
7
8
9
10
HKEY_CLASSES_ROOT
alert
(Default) = "URL:Alert Protocol"
URL Protocol = ""
DefaultIcon
(Default) = "alert.exe,1"
shell
open
command
(Default) = "C:\Program Files\Alert\alert.exe" "%1"

方案1

能看出这是一种比较通用的方案。能打开自定义的alert.exe,自然也能打开IE。所以只要将以下内容保存为test.reg,点击运行后就能将注册表项导入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Windows Registry Editor Version 5.00  

[HKEY_CLASSES_ROOT\openIE]
@="URL:OpenIE Protocol"
"URL Protocol"=""

[HKEY_CLASSES_ROOT\openIE\DefaultIcon]
@="iexplore.exe,1"

[HKEY_CLASSES_ROOT\openIE\shell]

[HKEY_CLASSES_ROOT\openIE\shell\open]

[HKEY_CLASSES_ROOT\openIE\shell\open\command]
@="cmd /c set m=%1 & call set m=%%m:openIE:=%% & call \"C:\\Program Files\\Internet Explorer\\iexplore.exe\" %%m%% & exit"

与微软官方范例的差别在于将协议改为了openIE://(这个不重要),以及最后的命令改为了一串很长的:

阅读全文 »

我们在上一篇已经将Dockerfile精简为了:

1
2
FROM nginx:alpine
COPY ./dist /usr/share/nginx/html

但相信你也发现了,内容中还是对web服务器有着强依赖。当我们想换成其他版本的Nginx镜像,或换成其他Web服务器,就必须修改源代码中的Dockerfile,重新制作镜像。
这种情况并非不可能。典型的场景之一:我们镜像所依赖的Nginx或tomcat版本出现了某个安全事故,而该问题可以通过将web服务器或web容器版本升级到最新版本解决。
所以我们希望能在Dockerfile中将Nginx的痕迹彻底抹除,只在Kubernetes的YAML中指定web服务器。

一个很自然的想法就是:我们使用一个默认的web服务器镜像。在使用该镜像的容器启动之前,将静态网站的文件拷贝到相应目录,就像在前一篇从ConfigMap获取配置文件一样。
这就是initContainer的作用。

1. initContainer

我们首先将镜像改为alpine,并修改COPY的路径:

1
2
FROM alpine:latest
COPY ./dist /html

重新编译为latest版本(这是为了强制每次重新拉取镜像):

阅读全文 »

虽然我们已经成功地将一个静态网站成功地在Kubernetes里部署起来了,但还有很多细节可以完善。我们就在这一节里逐步优化。

1. ConfigMap

问题最明显的是。重温一下我们静态网站之前使用的Dockerfile:

1
2
3
FROM nginx:alpine
COPY default.conf /etc/nginx/conf.d/default.conf
COPY ./dist /usr/share/nginx/html

首先是Nginx配置default.conf。
网站的源代码不应该干涉网站怎么部署。到底部署在Apache,Nginx还是Node.js,是否要在部署的时候添加自定义Header,都不该是开发者关注的事情。我们也不希望修改网站的timeout配置还需要动到源代码。从耦合性的角度来看,这个Nginx网站的配置文件不应该放到源代码中。
对于这类配置文件,Kubernetes里有专门的对象ConfigMap来保存。

从ConfigMap这个名字就可以猜得到,它存储的是配置信息,存储的格式是Map类型,即键值对。
配置信息可以是像本篇中的Nginx config配置,可以设置环境变量,可以是Java的properties和application.yml配置文件,可以是Redis和MySQL的配置文件。它很适合需要在一套Kubernetes集群上部署多个环境(例如特性分支/sit/uat)的情况。(当然我们的Java应用将使用Spring Cloud Config配置中心,所以目前不会用ConfigMap管理配置)
本篇POC的ConfigMap如下:

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
apiVersion: v1
kind: ConfigMap
metadata:
name: poc-web-config
labels:
app: poc-web
data:
default.conf: |
server {
listen 80;
server_name localhost;

charset utf-8;
#access_log /var/log/nginx/log/host.access.log main;

location / {
root /usr/share/nginx/html;
index index.html index.htm;
}

#error_page 404 /404.html;

# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

default.conf为key(键),下面的内容为value(值)。结构非常简单。
在Kubernetes中,ConfigMap是一种特殊的Volume(卷):Projected Volume。可以认为ConfigMap是Kubernetes中的数据被投射(Project)到容器中的。
关于Volume我们会在后续展开讨论,这里只是先提一下:要在容器中使用volume,需要先在spec中定义,然后mount到容器中。所以添加了ConfigMap后的Deployment定义YAML如下:

阅读全文 »

在上一篇中,通过NodePort模式,其实我们已经可以将一系列Pod暴露给集群外。但Service最多只能做好OSI 4层的负载均衡。而OSI 7层的负载均衡需要交给Ingress。
简单解释一下,2层的负载均衡就是虚拟MAC地址接收请求;3层的负载均衡就是虚拟IP地址;4层就是基于IP + 端口;7层就涉及URI等应用层。
以下文会提到的Traefik官方的一张图来说明,Ingress的作用就是根据不同的域名,正确找到对应的后台的Service:
Traefik作用
在Service接到请求后,再负责转交给Pod:
Kubernetes Service与Deployment

要使用Ingress需要先安装一个Ingress Controller。一般比较常用的有两个:Nginx Ingress Controller和Traefik Ingress Controller。在我做POC的过程中一开始选择Nginx Ingress Controller,但总是curl调不通,于是最终选择了Traefik。Traefik还多带了一个UI不错的后台管理admin dashboard。而Nginx的不少功能需要用Nginx Plus版的才有。虽然也很理解,毕竟Nginx也是要恰饭的。。。
需要说明一点:虽然Nginx本身可以同时担任静态网站web server和反向代理两种职责,但Nginx Ingress Controller只负责反向代理。
Ingress Controller除了这两个之外,还是有F5的、Kong的和Voyager等等。从这里就可以看到Kubernetes的一大特点:指定了方向,让各厂家和开源开发者发挥自己的特长来做实现。

1. Traefik Ingress安装

安装按照官方文档一步步做就行了。
先配置RBAC(关于RBAC我们之后详细介绍):

1
kubectl apply -f https://raw.githubusercontent.com/containous/traefik/v1.7/examples/k8s/traefik-rbac.yaml

然后配置Ingress Controller和对应的Service,对外暴露http的80端口和admin的8080端口。
Ingress Controller有两种部署方式:Deployment和DaemonSet。DaemonSet是一种特殊的Pod:

  • 在每个节点上有且仅有一个Pod实例
  • 当有新Worker节点加入时,自动在新节点上创建;旧节点被删除后,上面的DaemonSet Pod会被自动回收

两种方式各有各的好处。官方文档上都有详细比较,并在最后很贴心地对选择困难症给了建议:遇事不决先用DaemonSet试试。那么作为POC我就却之不恭选择DaemonSet了。反正要吃后悔药也就是几个命令的事情。

阅读全文 »

Deployment只是保证了支撑服务的Pod的数量,但是没有解决如何访问这些服务的问题。一个Pod只是一个运行服务的实例,随时可能在一个节点上停止,在另一个节点以一个新的IP启动一个新的Pod。因此不能以确定的IP和端口号提供服务。
要稳定地提供服务,需要服务发现和负载均衡能力。服务发现是一个微服务中很基础的概念,即当服务提供者网络发生变化时,服务消费者能及时获得最新的位置信息。对于k8s来说,服务提供者就是Pod,提供服务发现能力的是Service。Deployment和Service分别负责Pod的部署和访问策略,互不相关。
所以从下面这张图也可以看出来,Service和Deployment并不是上下层级的关系。

Kubernetes Service与Deployment

1. Service YAML

一个简单的Service YAML模板如下:

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Service
metadata:
name: <endpoint-name>
spec:
ports:
- port: <port>
name: <port-name>
targetPort: <target-port-number>
selector:
app: <app-name>

可以看到它是由selector.app来选择需要暴露的Pod。spec.ports.port是service对外暴露的端口,而targetPort是Pod的端口。默认使用TCP协议。
对于我们的POC的网站,service YAML定义如下:

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Service
metadata:
name: poc-web-service
spec:
ports:
- port: 80
name: web
targetPort: 80
selector:
app: poc-web

当我们查看已部署的service的时候,可以看到该service对应的ip:

阅读全文 »