程序部署环境的容器化已经是大势所趋,微服务为容器化提供了广阔的应用舞台,k8s已经把Docker纳入为它的底层支撑容器引擎,一统江湖,成为了容器技术事实上的标准。一般的应用程序是不能直接拿来部署到容器上的,需要经过一些修改才能移植到k8s上。那么这些改动包括哪些内容呢?

它主要有两个部分:

  • 第一部分是服务调用。不论是微服务之间的调用,还是微服务调用数据库或前端调用后端,调用的方式都是一样的。都需要知道IP地址,端口和协议,例如“http://127.0.0.1:80”, 其中“http”是协议,“127.0.0.1”是IP地址,“80”是端口。它的关键是让k8s的配置文件和应用程序都共享相同的调用地址。
  • 第二部分是数据的持久存储。在程序运行时,经常要访问持久存储(硬盘)上的数据,例如日志,配置文件或临时共享数据。程序在容器中运行,一旦出现问题,容器会被摧毁,k8s会自动重新生成一个与原来一模一样的容器,并在上面重新部署应用程序。在集群环境下,用户感觉不到容器故障,因为系统已经自动修复了。但当容器被摧毁时,容器上的数据也一起被摧毁了,因此要保证程序运行的连续性,就要让持久存储不受容器故障的影响。

程序实例:

我们通过一个Go(别的语言也大同小异)微服务程序做例子来展示要做的修改。它本身的功能非常简单,只是用SQL语句访问数据库中的数据,并写入日志。你可以简单地把它分成两层,后端数据访问层和数据库层。在k8s中它被分成两个服务。一个是后端服务程序,另一个是数据库(用MySQL)服务。后端程序要调用数据库服务,然后会把一些数据写入日志,而且这个日志不能因为容器故障而丢失。数据库对数据的保存要求更高,即使k8s集群或虚拟机出了问题或断电也要保证数据的存在。

上面是程序的目录结构。我们重点讲一下与k8s相关的。“config”目录包含与程序配置有关的代码,“logs”目录是用来存储日志文件的,没有代码。“script”目录是重点,里面包含了所有与部署程序相关的文件。其中“database”子目录里面是数据库脚本,“kubernetes”子目录存有k8s的所有配置文件,一回儿还会详细讲解。

服务调用:

服务调用涉及到两个不同的部分。一部分是k8s的配置文件,它负责服务的注册和发现。所有部署在k8s上的应用都通过k8s的服务来进行互相调用。另一部分是应用程序,它需要通过k8s的服务来访问其他程序。在没有k8s时,后端要想访问数据库,代码是这样的:

db, err := sql.Open("mysql", "dbuser:dbuser@tcp(localhost:3306)/service_config?charset=utf8")

其中,“dbuser:dbuser”是数据库用户名和口令,“localhost:3306”是数据库主机名和端口地址,“service-config”是数据库名,共有五个数据需要读取。迁移到k8s之后,我们要把这些参数从程序中提取出来,转化成从k8s中读取相关数据。

k8s配置:

先来看一下k8s的配置文件。

上面就是k8s的配置文件目录结构,最外层(kubernetes目录下)有两个“yaml”文件“k8sdemo-config.yaml”和"k8sdemo-secret.yaml",它们是被不同服务共享的,因此放在最外层。另外还有一个"k8sdemo.sh"文件是k8s命令文件,用来创建k8s对象。“kubernetes”目录下有两个子目录“backend”和“database”分别存放后端程序和数据库的配置文件。它们内部的结构是类似的,都有三个“yaml”文件:

  • backend-deployment.yaml:部署配置文件,
  • backend-service.yaml:服务配置文件
  • backend-volume.yaml:持久卷配置文件.

关于k8s的核心概念,请参阅“通过实例快速掌握k8s(Kubernetes)核心概念”. “backend”目录还多了一个“docker”子目录用来存储backend应用的Docker镜像,database的镜像文件直接从Docker的库中取得,因此不需要另外生成镜像文件。

k8s参数配置:

要想集成应用程序和k8s需要两个层面的参数共享,一个是应用程序和k8s之间的参数共享,另一个是不同k8s服务之间的参数共享。

k8s共享参数定义:

共享参数可以通过两种方式实现,一个是环境变量,另一个是持久卷。这两种方式大同小异,我们这里用环境变量的方式。这其中最关键的是“k8sdemo-config.yaml”和"k8sdemo-secret.yaml"这两个文件,它们分别存储了普通参数和保密参数。这些参数是属于整个应用程序的,被各个服务共享。

下面就是“k8sdemo-config.yaml”,它里面(在“data:”下面)定义了三个数据库参数,分别是数据库主机(MYSQL_HOST),数据库端口(MYSQL_PORT),数据库名(MYSQL_DATABASE)。

apiVersion: v1
kind: ConfigMap
metadata:
name: k8sdemo-config # ConfigMap的名字, 在引用数据时需要
labels:
app: k8sdemo
data:
MYSQL_HOST: k8sdemo-database-service # 数据库主机
MYSQL_PORT: "3306" # 数据库端口
MYSQL_DATABASE: service_config # 数据库名

下面就是“k8sdemo-secret.yaml”,它里面(在“data:”下面)也定义了三个数据库参数,根用户口令(MYSQL_ROOT_PASSWORD),普通用户名(MYSQL_USER_NAME),普通用户口令(MYSQL_USER_PQSSWORD)

apiVersion: v1
kind: Secret
metadata:
name: k8sdemo-secret
labels:
app: k8sdemo
data:
MYSQL_ROOT_PASSWORD: cm9vdA== # 根用户口令("root")
MYSQL_USER_NAME: ZGJ1c2Vy # 普通用户名("dbuser")
MYSQL_USER_PASSWORD: ZGJ1c2Vy # 普通用户口令("dbuser")

有关k8s的参数配置详细信息,请参阅“通过搭建MySQL掌握k8s(Kubernetes)重要概念(下):参数配置”.

引用k8s共享参数:

下面就是“backend-deployment.yaml”,它定义了“backend“服务的部署(Deployment)配置。它的“containers:”部分定义了容器,“env:”部分定义了环境变量,也就是我们所熟悉的操作系统的环境变量,一般是由系统来定义。不同的系统例如Linux和Windows都有自己的方法来定义环境变量。

apiVersion: apps/v1
kind: Deployment
metadata:
name: k8sdemo-backend-deployment
labels:
app: k8sdemo-backend
spec:
selector:
matchLabels:
app: k8sdemo-backend
strategy:
type: Recreate
template:
metadata:
labels:
app: k8sdemo-backend
spec:
containers: # 定义容器
- image: k8sdemo-backend-full:latest
name: k8sdemo-backend-container
imagePullPolicy: Never
env: # 定义环境变量
- name: MYSQL_USER_NAME
valueFrom:
secretKeyRef:
name: k8sdemo-secret
key: MYSQL_USER_NAME
- name: MYSQL_USER_PASSWORD
valueFrom:
secretKeyRef:
name: k8sdemo-secret
key: MYSQL_USER_PASSWORD
- name: MYSQL_HOST
valueFrom:
configMapKeyRef:
name: k8sdemo-config
key: MYSQL_HOST
- name: MYSQL_PORT
valueFrom:
configMapKeyRef:
name: k8sdemo-config
key: MYSQL_PORT
- name: MYSQL_DATABASE
valueFrom:
configMapKeyRef:
name: k8sdemo-config
key: MYSQL_DATABASE
ports:
- containerPort: 80
name: portname
volumeMounts:
- name: k8sdemo-backend-persistentstorage
mountPath: /app/logs
volumes:
- name: k8sdemo-backend-persistentstorage
persistentVolumeClaim:
claimName: k8sdemo-backend-pvclaim

k8s的环境变量主要是用来向容器传递参数的。环境变量引用了“k8sdemo-config.yaml”和"k8sdemo-secret.yaml"文件里的参数,这样就在k8s内部用过共享参数定义和参数引用实现了k8s层的参数共享。

下面是部署配置文件里的环境变量的片段。“ - name: MYSQL_USER_PASSWORD”是环境变量名,“secretKeyRef”说明它的值来自于secret,“name: k8sdemo-secret”是secret的名字,“key: MYSQL_USER_PASSWORD”是secret里的键名,它的最终含义就是环境变量“MYSQL_USER_PASSWORD”的值是由secret里的量“MYSQL_USER_PASSWORD”来定义。

 env:
- name: MYSQL_USER_PASSWORD
valueFrom:
secretKeyRef:
name: k8sdemo-secret
key: MYSQL_USER_PASSWORD

下面是另一个定义环境变量的片段,与上面的类似,只不过它的键值来自于configMap,而不是secret。

 env:
- name: MYSQL_DATABASE
valueFrom:
configMapKeyRef:
name: k8sdemo-config
key: MYSQL_DATABASE

关于k8s的部署配置细节,请参阅“通过搭建MySQL掌握k8s(Kubernetes)重要概念(上):网络与持久卷”. "

程序和k8s的参数共享:

k8s在创建容器时,会创建环境变量。应用程序在容器里运行时可以从环境变量里读取共享参数已达到应用程序和k8s共享参数的目的。下面就是Go程序访问数据库的代码片段。


type dbConfig struct {
dbHost string
dbPort string
dbDatabase string
dbUser string
dbPassword string
} func buildMysql() (dataservice.UserDataInterface, error) {
tool.Log.Debug("connect to database ")
dc := buildDbConfig ()
dataSourceName := dc.dbUser + ":"+ dc.dbPassword + "@tcp(" +dc.dbHost +":" +dc.dbPort +")/" + dc.dbDatabase + "?charset=utf8";
tool.Log.Debug("dataSourceName:", dataSourceName)
//db, err := sql.Open("mysql", "dbuser:dbuser@tcp(localhost:3306)/service_config?charset=utf8")
db, err := sql.Open("mysql", dataSourceName)
checkErr(err)
dataService := userdata.UserDataMysql{DB: db}
return &dataService, err
} func buildDbConfig () dbConfig{
dc :=dbConfig{}
dc.dbHost = os.Getenv("MYSQL_HOST")
dc.dbPort = os.Getenv("MYSQL_PORT")
dc.dbDatabase = os.Getenv("MYSQL_DATABASE")
dc.dbUser = os.Getenv("MYSQL_USER_NAME")
dc.dbPassword = os.Getenv("MYSQL_USER_PASSWORD")
return dc
}

上面程序中,“buildDbConfig()”函数从环境变量中读取k8s给容器设置好的参数,并上传给“buildMysql()”函数,用来连接数据库。上面是用Go程序读取环境变量,但其它语言例如Java也有类似的功能。

持久存储:

“backend”服务日志:

持久存储相对比较简单,它不需要做额外的应用程序修改 ,但需要程序和k8s相互配合来完成。

Go代码:

下面是日志设置的Go代码片段,它把日志的输出设为k8sdemo的logs目录和Stdout。

func RegisterLogrusLog() error {
//standard configuration
log := logrus.New()
log.SetFormatter(&logrus.TextFormatter{})
log.SetReportCaller(true)
file, err := os.OpenFile("../logs/demo.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
fmt.Println("Could Not Open Log File : ", err)
return errors.Wrap(err, "")
}
mw := io.MultiWriter(os.Stdout,file)
log.SetOutput(mw)
...
return nil
}

挂载持久卷:

下一步要做的就是挂载本地目录到容器的“logs”目录,这样日志在写入“logs”目录的时候就写入了本地目录。下面是生成k8s持久卷的配置文件“backend-volume.yaml”,它内部分成两部分(用“---”隔开)。上半部分是持久卷,下半部分是持久卷申请。它由本地硬盘的“/home/vagrant/app/k8sdemo/logs”目录生成k8s的持久卷。

apiVersion: v1
kind: PersistentVolume
metadata:
name: k8sdemo-backend-pv
labels:
app: k8sdemo-backend
spec:
capacity:
storage: 1Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
storageClassName: standard
local:
path: /home/vagrant/app/k8sdemo/logs
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- minikube
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: k8sdemo-backend-pvclaim
labels:
app: k8sdemo-backend
spec:
accessModes:
- ReadWriteOnce
# storageClassName: local-storage
resources:
requests:
storage: 1Gi #1 GB

下面是“backend-deployment.yaml”部署文件片段,它把k8s的持久卷挂载到容器的“app/logs”上。

          volumeMounts:
- name: k8sdemo-backend-persistentstorage
mountPath: /app/logs
volumes:
- name: k8sdemo-backend-persistentstorage
persistentVolumeClaim:
claimName: k8sdemo-backend-pvclaim

完成之后,就可以在本地目录上查看日志文件,这样即使容器或k8s集群出现问题,日志也不会丢失。

为什么目录是“app/logs”呢?因为在生成“beckend”的镜像时,设定的容器的运行程序根目录是“app”。关于如何创建Go镜像文件,请参阅“创建优化的Go镜像文件以及踩过的坑”.

数据库持久卷:

Mysql数据库的持久卷设置与日志类似,详情请参阅“通过搭建MySQL掌握k8s(Kubernetes)重要概念(上):网络与持久卷”.

存在的问题:

细心的读者可能已经发现了,在定义的环境变量中,有两个与其他的有些不同,这两个就是“MYSQL_HOST”和"MYSQL_PORT"。所有的环境变量都是在参数文件(k8sdemo-config.yaml)中定义,别的环境变量是在k8s配置文件(例如backend-deployment.yaml)中引用,但这两个虽然在k8s的部署配置文件提到了,但只是用来定义环境变量,最终只是被应用程序引用了,但服务的配置文件并没有真正引用它。

apiVersion: v1
kind: Service
metadata:
name: k8sdemo-database-service # 这里并没有引用环境变量
labels:
app: k8sdemo-database
spec:
type: NodePort
selector:
app: k8sdemo-database
ports:
- protocol : TCP
nodePort: 30306
port: 3306 # 这里并没有引用环境变量
targetPort: 3306

上面是数据库服务的配置文件“database-service.yaml”, 这里并没有引用“MYSQL_HOST”和"MYSQL_PORT",而是直接写上“k8sdemo-database-service”和“3306”。为什么会是这样呢?因为k8s的环境变量是有局限性的,它只能定义在“containers:”里面,也就是说只有容器才能定义环境变量,这从理论上也说得过去。因为如果没有容器,那么环境变量定义给谁呢?但这就导致了服务名不能引用配置参数,结果就是服务名要在两处被定义,一个是参数文件,另一个是服务配置文件。如果你要修改它,就要在两处同时修改,加大了出错的几率。有什么办法可以解决呢?

Helm

这在k8s内部是没法解决的,但在k8s外是可以解决的。有一个很流行的k8s的包管理工具,叫“helm”, 能够用来定义服务变量。

下面就是使用了Helm之后的Pod的配置文件。

alpine-pod.yaml

apiVersion: v1
kind: Pod
metadata:
name: {{ template "alpine.fullname" . }}
labels:
# The "app.kubernetes.io/managed-by" label is used to track which tool deployed a given chart.
# It is useful for admins who want to see what releases a particular tool
# is responsible for.
app.kubernetes.io/managed-by: {{ .Release.Service }}
# The "app.kubernetes.io/instance" convention makes it easy to tie a release to all of the
# Kubernetes resources that were created as part of that release.
app.kubernetes.io/instance: {{ .Release.Name | quote }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
# This makes it easy to audit chart usage.
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
app.kubernetes.io/name: {{ template "alpine.name" . }}
spec:
# This shows how to use a simple value. This will look for a passed-in value called restartPolicy.
restartPolicy: {{ .Values.restartPolicy }}
containers:
- name: waiter
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
command: ["/bin/sleep", "9000"]

下面是变量的定义文件values.yaml

image:
repository: alpine
tag: latest
pullPolicy: IfNotPresent restartPolicy: Never

程序来源

Helm使用了Go的模板(template)。模板是用数据驱动的文本生成器。它在文本模板里用特殊符号(这里是“{{ }}”)定义变量或数据,然后在执行模板时再将变量转换成变量值,生成最终文本,一般在前端用的比较多。在Helm模板里,“{{ }}”里面的就是变量引用,变量是定义在“values.yaml”文件里的。

上面的例子有两个文件,一个是“alpine-pod.yaml”,另一个是“values.yaml”。变量定义在“values.yaml”里,再在“alpine-pod.yaml”文件里引用,这样就解决了k8s的环境变量的局限性。

Helm是功能非常强大的k8s包管理工具,而且可以简化容器部署,是一款非常流行的工具。但它的问题是Helm增加了配置文件的复杂度,降低了可读性。现在的版本是Helm2,但Helm3不久就要出炉了。Helm3有一个功能是支持Lua模板,能直接用对象编程(详情请见A First Look at the Helm 3 Plan),新的模板比现在的看起来要强不少,如果你想使用新的还需要再等一等。

结论:

一般的应用程序是不能直接部署到k8s上的,需要经过一些改动才行。它主要有两个部分。第一个是服务调用。第二个是数据的持久存储。服务调用的关键是让k8s和应用程序共享参数。k8s里已经有这种机制,但它还有一点缺陷,只能用来定义容器的环境变量,需要引入其他工具,例如Helm才能解决这个问题。持久存储不需要修改程序,但需要k8s的配置和应用程序配合才能成功。

源码:

完整源码的github链接

备注:

本文中的Go程序只是示例程序,只有k8s配置文件部分是认真写的,可以直接拷贝或引用。其他部分都是临时拼凑来的,主要是为了作为例子,因此没有花时间完善它们,总的来说它们写得比较粗糙,千万不要直接拷贝。

索引:

  1. 通过实例快速掌握k8s(Kubernetes)核心概念
  2. 通过搭建MySQL掌握k8s(Kubernetes)重要概念(上):网络与持久卷
  3. 通过搭建MySQL掌握k8s(Kubernetes)重要概念(下):参数配置
  4. helm/helm
  5. Alpine: A simple Helm chart
  6. A First Look at the Helm 3 Plan

本文由博客一文多发平台 OpenWrite 发布!

如何把应用程序移植到k8s的更多相关文章

  1. STM32F429 LCD程序移植

    STM32F429自带LCD驱动器,这一具有功能给我等纠结于屏幕驱动的程序员带来了很大的福音.有经验的读者一定有过这样的经历,用FSMC驱动带由控制器的屏幕时候,一旦驱动芯片更换,则需要重新针对此驱动 ...

  2. 嵌入式linux应用程序移植方法总结

    嵌入式linux应用程序移植方法总结 前段时间一直在做openCapwap的移植和调试工作,现在工作已接近尾声,编写本文档对前段工作进行一个总结,分享下openCapwap移植过程中的经验和感悟.江浩 ...

  3. linux第三方程序移植

    摘要:在linux开发过程中经常需要用到第三方的程序,有时需要用到它们的库,有时需要它们生成的可执行文件,如何正确地编译这些第三方的程序,以方便地使用和开发自己需要的程序,将是本文要论述的内容. 1. ...

  4. 【转】将 Linux 应用程序移植到 64 位系统上

    原文网址:http://www.ibm.com/developerworks/cn/linux/l-port64.html 随着 64 位体系结构的普及,针对 64 位系统准备好您的 Linux® 软 ...

  5. STM32F407使用MFRC522射频卡调试及程序移植成功

    版权声明:转载请注明出处,谢谢 https://blog.csdn.net/Kevin_8_Lee/article/details/88865556 或  https://www.cnblogs.co ...

  6. Linux64位程序移植

    1 概述 Linux下的程序大多充当服务器的角色,在这种情况下,随着负载量和功能的增加,服务器所使用内存必然也随之增加,然而32位系统固有的4GB虚拟地址空间限制,在如今已是非常突出的问题了:另一个需 ...

  7. 017_STM32程序移植之_AS608指纹模块

    STM32程序移植之AS608指纹模块 BUG说明: 硬件接线图如图所示 STM32引脚 指纹模块引脚 功能 3.3V 3.3V PA3 Tx PA2 Rx GND GND PA1 WAK 3.3V ...

  8. 016_STM32程序移植之_舵机

    STM32程序移植之舵机PWM测试 接线图如下: STM32引脚 舵机引脚 功能 GND GND 正极电源 具体看舵机的额定电压 PA6 PWM引脚 STM32引脚 CH340引脚 GND GND 3 ...

  9. 015_STM32程序移植之_NRF24L01模块

    STM32程序移植之NRF24L01模块 引脚接线图如下所示 STM32引脚 NRF24L01引脚 功能 GND GND 3.3V 3.3V PB8 CE PB9 CSN PB13 SCK PB15 ...

随机推荐

  1. MOOC C++笔记(一):从C到C++

    第一周:从C到C++ 引用 概念 类型名&引用名=某变量名 某个变量的引用,等价于这个变量,相当于该变量的别名 注意事项 1.定义引用时一定要将其初始化成引用某个变量. 2.初始化后,它就一直 ...

  2. PyCharm2019激活

    PyCharm下载地址:https://www.jetbrains.com/pycharm/download/ 永久激活 这里主要介绍永久激活的方式,永久激活后,就可以放心使用了,一劳永逸,5分钟就能 ...

  3. vue2.0生成二维码图片并且下载图片到本地兼容写法

    vue生成二维码图片,这里使用的是qrcode.js 这个插件(亲测写法,兼容没有问题) 第一步,下载插件 需要注意,这里下载的是qrcodejs2 cnpm install --save qrcod ...

  4. seo搜索引擎的优化方法

    现在互联网的入口,一般都是被搜索引擎霸占.所以我们要想让别人搜索时,优先看到我们的网站.有两种方法: 1.竞价排名.这是需要钱的,给的钱越多,排名越靠前.参考某度.. 2.不想花钱,就使用seo搜索引 ...

  5. Spring框架学习笔记(2)——面向切面编程AOP

    介绍 概念 面向切面编程AOP与面向对象编程OOP有所不同,AOP不是对OOP的替换,而是对OOP的一种补充,AOP增强了OOP. 假设我们有几个业务代码,都调用了某个方法,按照OOP的思想,我们就会 ...

  6. Scala Class etc.

    Classes 一个源文件可包含多个类,每个类默认都是 public 类字段必须初始化,编译后默认是 private,自动生成 public 的 getter/setter :Person 示例 pr ...

  7. 读《深入理解Elasticsearch》点滴-改正用户拼写错误

    1.使用“建议”的方法:在query body的json结构体中,增加suggest节点:或者使用特殊的REST端点 2.es自带有多个不同的suggest实现,用来纠正用户的拼写错误及创建自动补全等 ...

  8. Mysql INSTR函数

    在Mysql中,可以使用INSTR(str,substr)函数,用于在一个字符串(str)中搜索指定的字符(substr),返回找到指定的字符的第一个位置(index),index是从1开始计算,如果 ...

  9. json与java对象的转换,以及struts2对json的支持,实现ajax技术

    这两天学的东西有点多,今天抽个时间写下来,以此作为激励,这两天学了json,ajax,jQuery 一.使用第三方的工具java转换为json类型 首先就是java类型转换为json对象,首先要导入第 ...

  10. 两小无猜的爱恨情仇--java =+和+=揭秘

    故事背景 当一个人问另一个人“敢不敢”的时候,另一个人必须说“敢”,这就是游戏的规则.小男孩朱利安和小女孩苏菲的相遇即开始于这样一场孩童的闹剧,一个精美的铁盒子就是他们游戏的见证.说脏话,扰乱课堂,在 ...