Mechanism
Linux容器是指一个或者一组从系统中隔离出来的进程,运行此进程的所有文件均由一个image提供。由此定义我们知道容器包含两个点一是隔离,二是运行文件的封装。其中隔离包含cpu,memory隔离,网络隔离,文件系统隔离,设备可见性隔离等。下面来分段介绍windows平台如何实现以上几种隔离。文件封装沿用docker image本文不再详细阐述。内容较多使用Windows容器运行UE4渲染任务会单独拆分一个文章介绍。
PS. 由于Windows 容器特性以及k8s对其的支持都处于快速迭代的状态本文内容主要基于 k8s v1.20.x和windows server lts 2019.
Isolation
windows主要支持两种隔离方式:一种是模拟Linux cgroup的原理实现了一套namespace机制以实现进程级别的隔离共享windows内核。一种是Hyper-V隔离,通过Hyper-V启动优化过的轻量化虚拟机来运行每个容器类似与kata runtime的思路,每个虚拟机有自己独立的内核,可以运行与host不同版本的windows虚拟机以及运行Linux容器(除了Azure应该没人这么干,k8s也不支持hyper-v隔离)以下是两种隔离模式的架构。
由于windows闭源的特性很多信息都是隐藏,以下部分主要基于以下几篇文章1,2,3,4,5。微软在dockecon 16上有对windwos容器原理的介绍,感兴趣的可以去Google一下视频 背景知识 windows NT kernle中的processes management JobObjects, which is a kernel object that allows you to manage a group of processes as a single unit. 对job objects的修改会对job内的所有进程生效。 以下几种limits sets: UI相关限制比如,读写clipboard等 CPU使用频率,设置允许使用的最大CPU time 进程优先级,jobs内的所有进程处于一个相同的优先级 完整的API可以参考win32文档
- namespace 在windows os中并不叫做namespace,而是叫做silo, 是一种对windows job object的扩展,是对一组进程的限制,支持 注册表,进程ID,文件系统,网络, object namespace上的隔离
- object namespace 是windows中用来保护object的非授权访问机制在windows nt内核中 c:windows 其实是 DosDevicesC:windows,这里包含了windows所有对象的访问endpoint(系统级object namespace对用户隐藏)。silo提供了一种类似与chroot的隔离方案,会将容器的DosDevicesC: 映射到SilosfooDosDevicesC:
windows引入了 host compute service(HCS)来管理容器,并且提供了golang SDK
Network
Windows 容器网络方案使用来自于Hyper-V的虚拟网络技术。但是不像Hyper-V虚拟机可以让用户自己管理虚拟机网卡,虚拟交换机设定各种参数。容器环境下使用Host Compute Network(HCN)一套接口来管理容器网络。这里借用一下官网的汇总架构图
HCN会为每个network创建一个专用的hyper-v virtual switch,并且管理NAT策略以及IP池。启动容器时HCN会在每个network namespace 下面创建一个container endpoint(除NAT模式下每个endpoint对应一个vNIC),并且下发IP,DNS,routes规则。
容器运行时使用HCN初始化好的namespace来调用HCS创建容器。
HCN支持如下的网络模式
Network Type | Container to Container(same node) | Container to external | Contaienr to Container(cross node) | IPAM |
---|---|---|---|---|
NAT | 每个容器一个VNIC挂到Vswitch上不可与host network冲突 同Vswitch的容器可以通信 | 全部流量走host nic NAT 使用winNAT模块 | 不支持 | HCN管理IP |
Transparent | 每个容器一个VNIC与host netnetwork在同一个三层网络中 | 容器内流量直接在物理网络上路由 | 直接通过物理网络通信 | 静态IP或物理网络提供DHCP |
Overlay | 同子网内通过vswitch直接交换 跨子网通过本机NIC路由 | 不直接支持,需要一个虚拟endpoint提供NAT策略 | VXLAN封装之后通过NIC路由 | 由具体的CNI实现IP管理 |
L2Bridge | 同子网通过hyper-v switch通信 跨子网改写mac后由host路由 | 出入流量在host nic上做二层mac重写 | 重写mac之后由host nic进行路由 | HCN管理IP |
L2Tunnel(Azure only) | azure直接下发hyper-v switch策略 | 通过azure virtual network gateway处理 | 同单一node内通信类似 | azure cni |
自己创建network的例子:
代码语言:text复制$network = New-HNSNetwork -Type $Global:NetworkModel -DNSServer "183.69.83.19,183.60.82.98" -AddressPrefix "169.254.255.248/29" -Gateway "169.254.255.249" -Name $NetworkName -Verbose
# if use l2bridge, create a quasi gateway for the containers to use
if ($Global:NetworkModel -eq "L2bridge") {
# Enable forwarding
netsh int ipv4 set int "vEthernet (Ethernet)" for=en
$hnsEndpoint = New-HnsEndpoint -NetworkId $network.ID -Name $endpointName -IPAddress 169.254.255.250 -Verbose
Attach-HnsHostEndpoint -EndpointID $hnsEndpoint.Id -CompartmentID 1
netsh int ipv4 set int $vnicName for=en
# add a static ARP entry with a dummy MAC so that traffic is able to leave our host without being stuck waiting for an ARP probe to resolve this gateway IP
netsh int ipv4 add neighbors $vnicName "169.254.255.249" "00-01-e8-8b-2e-4b"
}
route delete 169.254.0.0
# after create hns network, a route of 169.254.0.0/17 must be added
route add 169.254.0.0 mask 255.255.0.0 $podGW metric 10
route -p add 169.254.0.0 mask 255.255.0.0 169.254.255.249
Storage
Windows文件系统也非常复杂。随着不断的添加新的特性进去,Transactions, File IDs, USN journal 等等,而Windows程序很多依赖于这些特性。
因此如果要基于此去做一个Linux世界中的AUFS,或者OverlayFS,但是支持上面这些NTFS特性的,极为困难。于是在 Windows Container v1 中,采用了类似于Device Mapper的办法 ,所不同的是,块设备是虚的,所以是 虚拟块设备 每个容器的NTFS 分区 的方式。使用 symlink 到宿主文件系统各层,这样让块设备不会过于臃肿。
K8S中存储方案主要转向CSI。CSI设计中每个node需要一个daemonset pod或者sidecar 容器来完成volume 格式化,挂载等操作,pod需要具备privileged或者file system admin权限。而Windows中并不支持对一个容器进行提权操作,所以在Windows 平台上产生了csi-proxy 方案
入上图所示简单来说这个方案是在容器所在的windows node上单独运行一个cis-proxy的进程来完成对disk,volume的一系列操作,规避掉容器内权限不足的问题。
csi-proxy方案只是一个无奈的折衷。从kubelet到最终执行操作的csi-proxy.exe走了四层的grpc io,链路冗长,而且csi-proxy的实现还是golang cmd.exec powershell command实现,性能不高,错误处理依赖文本处理。并且添加一个特性需要修改三个项目,维护成本极高。
在k8s 支持windows host container
PS. kubernetes 1.23 中windows host process container(windows server lts 2022)特性进入beta,csi-proxy变得比较鸡肋。个人感觉社区未来会放弃这个方案 PPS. windows host process container允许容器进行mount操作。同时支持host network、runasuser、runasnonroot
Devices
在Linux平台devices只需要将/dev/下的设备描述符文件mount进入容器便可以实现仅某个容器的设备可见。在windows 平台则要分情况来看
对于Hyper-v隔离的容器,无论容器是Linux还是windows都无法支持任何硬件设备挂入容器
对于process-isolated容器 则有条件支持硬件设备挂入
1.版本要求:
windows host os 2019 or windows 10, version 1809 or newer container base image must be 1809 or later docker enginer 19.03 or newer, containerd must include this PR
2.使用设备的 device interface class GUID挂载设备
简单来说device interface class guid 是用来标记某一类别硬件驱动的唯一id,需要注意的是这里标记的是驱动的类别而不是某个硬件设备。所以把一个驱动的class guid mount进入容器之后这类驱动之下的所有设备都会对一个容器可见。无法像Linux一样实现单一硬件设备的可见性隔离。
支持的设备类型和GUID如下:
Device Type | Interface Class GUID |
---|---|
GPIO | 916EF1CB-8426-468D-A6F7-9AE8076881B3 |
I2C Bus | A11EE3C6-8421-4202-A3E7-B91FF90188E4 |
COM Port | 86E0D1E0-8089-11D0-9CE4-08003E301F73 |
SPI Bus | DCDE6AF9-6610-4285-828F-CAAF78C424CC |
DirectX GPU Acceleration | 5B45201D-F2F2-4F3B-85BB-30FF1F953599 |
全部的class guid可以查阅MicrosoftDocs/windows-driver-docs
对于GPU还有特殊要求,上表中GPU一行设备类型是 DirectX GPU
也就是windows容器必须使用支持DirectX的GPU驱动才可以,并且用户程序也必须是使用DirectX框架访问GPU,其它类型一律不支持。
PS. NVIDIA Windows Data center driver 并不支持DX,这里必须使用GRID driver,GRID driver需要另外收取授权费用。
serverless tke(eks)上使用的windows os已经部署了device-plugin
来支持nvidia gpu的使用
Practice
Use Windows container on serverless tke cluster
windows 容器正式上线之后可以在页面操作添加windows节点 仅 1.20 集群支持Windows容器,更高版本正在接入中
- TKE集群添加超级节点池并添加一个按量超级节点(serverless tke 集群忽略)
- 使用以下yaml模板,创建windows node。对应值可以从1中添加的超级节点或者serverless集群中其它节点获取
apiVersion: v1
kind: Node
metadata:
annotations:
eks.tke.cloud.tencent.com/subnet-id: $SUBENET # subnet-xxxxxxx
eks.tke.cloud.tencent.com/zone-id: "$ZONEID" # 数字化的zone id
labels:
beta.kubernetes.io/arch: amd64
beta.kubernetes.io/os: windows
eks.tke.cloud.tencent.com/subnet-id: $SUBENET # subnet-xxxxxxx
eks.tke.cloud.tencent.com/version: v2
eks.tke.cloud.tencent.com/zone-name: ap-$REGION-$ZNOENUMBER # ap-guangzhou-7
failure-domain.beta.kubernetes.io/region: $REGIONNAME # gz
failure-domain.beta.kubernetes.io/zone: "$ZONEID" # 数字化的zone id
kubernetes.io/arch: amd64
kubernetes.io/hostname: eklet-$SUBENET # subnet-xxxxxxx
kubernetes.io/os: windows
node.kubernetes.io/windows-build: '10.0.17763'
node.kubernetes.io/instance-type: eklet
topology.com.tencent.cloud.csi.cbs/zone: ap-$REGION-$ZNOENUMBER # ap-guangzhou-7
topology.kubernetes.io/region: $REGIONNAME # gz
topology.kubernetes.io/zone: "$ZONEID" # 数字化的zone id
name: eklet-$SUBENET-win # subnet-xxxxxxx
spec:
taints:
- effect: NoSchedule
key: os
value: windows
- 创建runtime class
apiVersion: node.k8s.io/v1
handler: runhcs-wcow-process
kind: RuntimeClass
metadata:
name: windows
scheduling:
nodeSelector:
kubernetes.io/arch: amd64
kubernetes.io/os: windows
tolerations:
- effect: NoSchedule
key: os
operator: Equal
value: windows
以上就可以在此集群中创建windows 容器了:
代码语言:yaml复制kind: Pod
apiVersion: v1
metadata:
name: test-pod-1
spec:
runtimeClassName: windows
containers:
- name: test-container-1
env:
- name: MY_NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
image: mcr.microsoft.com/windows/servercore/iis:windowsservercore-ltsc2019
ports: # 为了告知windows cni创建 port-mapping规则,需要显示的写上希望暴露到pod ip上的端口
- containerPort: 80
- containerPort: 443
restartPolicy: Always
Image cache
windows容器正式上线之后可以通过前端管理镜像缓存,目前需要通过CLI来创建,其它操作可以从web端处理。
Windows base image 往往都在7G以上,而容器中的可用空间只有 19.9G,算上解压时需要的临时空间,所以tke上windows仅支持最大8G的镜像。即使8G以内镜像启动之后随着运行也会导致C盘爆满,os无法正常运行。
为了规避这个问题推荐windows容器都启用镜像缓存功能,详细描述参考。
创建例子如下,二进制在本文附件中
代码语言:shell复制./tkeclient image-cache create --region ap-xxxxxx --secretID XXXXXXX --secretKey XXXXXXX --images mcr.microsoft.com/windows/servercore/iis:windowsservercore-ltsc2019 --name windows-images-cache-01 --os windows --size 100 --subnetID subnet-xxxx --vpcID vpc-xxxx --imageUser xxxxxxxx --imagePasswd xxxxx
--size 的单位是G 最好设置比镜像大一倍,至少是image size 最大的5个层的size,否则可能遇到解压镜像层空间不足
--images 指定需要缓存的image,image base windows image必须是 1809或者server 2019
Pod with CFS
0.创建CFS,创建时选择SMB协议,不要使用NFS。
- 创建csidriver
apiVersion: storage.k8s.io/v1beta1
kind: CSIDriver
metadata:
name: smb.csi.k8s.io
spec:
attachRequired: false
podInfoOnMount: true
- 创建pv
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-smb-guest
spec:
capacity:
storage: 80Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
mountOptions:
- dir_mode=0777
- file_mode=0777
- vers=3.0
- guest
csi:
driver: smb.csi.k8s.io
readOnly: false
volumeHandle: public-108 # make sure it's a unique id in the cluster
volumeAttributes:
source: "//192.168.0.108/public"
- 创建pvc
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: pvc-smb-guest
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 80Gi
volumeName: pv-smb-guest
storageClassName: "" # 避免匹配到默认stroageClass
- 创建pod mount pvc
kind: Pod
apiVersion: v1
metadata:
name: test-pod-2
annotations:
spec:
runtimeClassName: windows
containers:
- name: test-container-1
env:
- name: MY_NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
image: mcr.microsoft.com/windows/servercore/iis:windowsservercore-ltsc2019
ports:
- containerPort: 80
- containerPort: 443
volumeMounts:
- mountPath: /data
name: smb
restartPolicy: Always
volumes:
- name: smb
persistentVolumeClaim:
claimName: pvc-smb-guest
更多参数参考csi-driver-smb