rhce8 question

记录一下上课笔记

  1. 进入nfs,autofs目录卡住,如何退出,应该是进入了D状态
  2. chrome 需要笔记本插件,js控制插件
  3. 修改root密码时,在linux 选项后面添加 rd.break 然后默认进入内核根目录,然后rw重新挂载系统根目录 /sysroot/
  4. fdisk 设置分区类型: 在创建分区结束后,使用t来标记文件类型, 然后使用L查看类型代码,查找到Linux swap 对应的代码是85

RHCIA

修改了网络信息之后,必须使用nmcli connection down和up connection才会生效

1
2
nmtui connection down Wired_connection1
nmtui connection up Wired_connection1

配置httpd服务时,修改port后,因为port非标准端口,需要用semanage打tag。否则失败,可以查看journeyctl -e 查看到错误信息和解决办法

1
semanage port -a -t http_port_t -p tcp 82

创建/home/admins 目录,并设置在目录中创建的文件,其组所有权自动设置为 adminusr。 这里就是设置s位,有数值方法和+-法

1
2
3
4
5
6
7
8
chmod 2770 /home/admins

# 或

chmod o-x /home/admins
chmod o-r /home/admins
chmod g+w /home/admins
chmod g+s /home/admins # 这里设置组的s位

验证方法,进入目录创建文件,查看组名

autofs 挂载有两个关键,第一在/etc/auto.master.d/ 目录下创建后缀名为autofs的文件, 第二是编辑挂载配置文件

内容格式

1
2
3
# cat /etc/auto.master.d/indirect
#主挂载点 挂载配置
/internal /etc/auto.demo

内容配置,* 表示所有目录(根据nfsserver的情况), 和&搭配使用

1
2
3
# cat /etc/auto.demo
# 间接挂载目录 挂载配置 挂载路径(nfs地址)
* -rw,fstype=nfs4,sync servera:/shares/indirect/&

setfacl 和getfacl 设置和显示文件属性

1
2
3
4
设置natash用户读写权限
setfacl u:natasha:rw /var/tmp/fstab
设置harry用户没有任何权限
setfacl u:harry:- /var/tmp/fstab

重置root密码,在linux 行末追加 rd.local, 按ctrl+x 启动

重新挂载系统根目录, chroot到根目录和修改密码

1
2
3
mount -orw,remount /systemroot/
chroot /systemroot/
passwd

逻辑卷扩容

设置分区start位置的时候,输入2048s。 s指扇区,一个扇区为512字节, 2048s指1M,这1M的分区为esp分区

扩容命令lvextrend,这个扩容只是将逻辑卷扩容, 而题目要求对文件系统扩容, 还需要执行文件系统类型对应的扩容命令如resize2fs(ext), xfs_growfs(xfs)。
但对lvextend命令添加 -r 参数可以一步到位

1
2
# 扩容到300MB
lvextend -r -L 300MB /dev/new_redhat/lv1

创建逻辑分区, 注意避免磁盘不够用,尽量共用上一题的磁盘。而且创建卷组的时候不要忘记-s参数

题目可能不是直接给分区大小,而是给出扩展块大小和拓展块个数,其实分区大小=拓展块大小 × 拓展块个数。 在lvcreate -l 来指定块个数

这时必须使用msdos分区类型才有主分区和拓展分区,而且为拓展分区分配剩余的所有空间。 最后在拓展分区上创建逻辑分区, 并将创建的逻辑分区加入卷组

创建分区,大小通过上面的算法计算出来

1
2
3
4
parted 
mkpart
print
exit

创建卷组

1
vgcreate -s 20MB -n npgroup /dev/vdb2

创建逻辑卷, 注意是小写l

1
lvcreate -l 45 -n np npgroup

这样就创建了一个 20×45 = 900MB的逻辑卷

创建新的swap分区,注意需要修改fstab, 而且验证的时候,不需要重启系统,而是执行

1
2
3
systemctl daemon-reload 
swapon -a # 类似mount -a
swapon --show

vdo –vdoLogicalSize 5G, 不能使用GB

1
vdo create --name test --vdoLogicalSize 5G --device /dev/vdb

blkid 查到 UUID, 最后添加到fstab

vdo 持久挂载 mount

1
default,x-systemd.requires=vdo.service

RHCE

ansible的有用命令
ansbile-doc -l | grep 查找模块和介绍
ansible-doc 模块 查看模块具体说明

创建inventory 文件, 子组的写法为

[webservices:children]
prod

prod 是webserverices组的子组

创建ansible.cfg 文件,可以复制 /etc/ansbile/ansible.cfg 到~/ansbile/目录下

容易忘记的选项是远程登陆身份, 即配置 remote_user = devops
如果环境没有配置免密登陆,需要在[defaults] 栏添加 ask_pass = True

验证方法 ansible all -m ping

ansbile ad-hoc 添加yum 源 ansible 模块的使用方法 ansible 主机名 -m 模块 -a ‘参数’

脚本要添加可执行权限

1
2
3
4
5
6
#!/bin/bash

# 先要切换到工作目录
cd /home/devops/ansible/

ansible all -m yum-repository -a 'name= description= baseurl= enabled= gpgcheck= gpgkey= '

安装软件包

安装软件包如’Development Tools’, 需要在包名前面加‘@’即 ‘@Development Tools’
验证yaml 语法格式: ansible-playbook --syntax-check *.yml

1
2
3
4
5
6
7
- name: install  development tools
hosts: dev
tasks:
- name: install development tools
yum:
name: "@Development Tools"
state: present

同步时间依赖角色,安装角色包’rhel-system-roles’ 需要在配置文件里添加上roles路径

playbook 示例在/usr/share/ansibe/roles/rhel-system-roles.timesync/Readme.md
现成的yml示例在/usr/share/doc/rhel-system-roles/timesync/example-timesync-playbook.yml

1
2
3
# ansible.cfg
[default]
roles_path = /home/devops/ansible/roles:/usr/share/ansible/roles

使用ansible-galaxy 命令安装角色,安装到本地roles目录

yml文件很简单, 执行命令 ansible-galaxy install -r roles/requirement.yml -p roles/
-p 表示安装路径

1
2
- name: balancer
file: /home/devops/ansible/files/5/haproxy.tar

创建apache角色, 角色对应一系列的任务和模板文件

通过ansible-galaxy init apache 来初始化角色目录,修改角色目录下的tasks/mail.yml 来添加任务,任务所需的模板文件在同级目录templates下
通过ansible servera -m setup 来查询环境变量, 在模板文件中使用ansible_facts['fqdn'] 来引用变量
题目说的hostname 是serverc.lab.example.com 对应变量是fqdn, 另外一个是ip即 [‘default_ipv4’][‘address’]

例如,安装httpd用到yum模块, 启用httpd 用到service模块, 防火墙配置用到firewalld模块

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
# home/devops/ansible/role/apache/tasks/main.yml

---
- name: install httpd
yum:
name: httpd
state: present

- name: enable httpd
service:
name: httpd
state: started
enabled: yes

- name: firewalld config
firewalld:
service: httpd
immediate: yes
permanent: yes
state: enabled

- name: install templates
template:
src: index.html.j2
dest: /var/www/html/index.html

模块文件放到templates目录下

1
2
# home/devops/ansible/role/apache/templates/index.html.j2
Welcome to {{ ansible_facts['fqdn'] }} on {{ ansible_facts['default_ipv4']['address'] }}

最后还需要创建一个playbook, 如同其他playbook 一样执行

1
2
3
4
5
6
# newrole.yml
---
- name: install new role
hosts: webservers
roles:
- apache

执行balancer 角色, 创建一个负载均衡

验证:
ssh到serverd 上,curl localhost, 先是访问的serverb, 然后访问的serverc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
---
- name: deploy apache on webservers
hosts: webservers
roles:
- apache

- name: deploy balancer on balancer
hosts: balancers
roles:
- balancer

- name: deploy phpinfo on webservers
hosts: webservers
roles:
- phpinfo

可能出现的错误,提示应该使用bind,这是由于balance role的template 错误

修改办法, 将listen行换行并加上bind

1
2
3
4
5
6
7
8
# /home/devops/ansible/roles/balancer/templates/haproxy.cfg.j2

backend app
{% for host in groups.balancers %}
- listen {{ daemonname }} 0.0.0.0:{{ listenport }}
+ listen {{ daemonname }}
+ bind 0.0.0.0:{{ listenport }}
{% endfor %}

创建逻辑卷,考验block/rescue/always, 使用lvol,when 和play对齐

使用了block/rescue/always就不需要tasks关键词

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
---
- name: create a logic volume with size of 5000 and format
hosts: all
block:
- name: create logic volume
lvol:
vg: research
lv: data
size: 1500
- name: fomat data volume
filesystem:
fstype: ext4
dev: /dev/research/data
rescue:
- name: debug message
debug:
msg: can't create a size of 1800
- name: create a logical volume with size of 800 and format
lvol:
vg: research
lv: data
size: 1500
- name: fomat data volume
filesystem:
fstype: ext4
dev: /dev/research/data
when: ansible_facts['lvm']['vgs']['research'] is defined
ignore_errors: yes
- name: debug 2
debug:
msg: volume group does not exist
when: ansible_facts['lvm']['vgs']['research'] is undefined

创建hosts文件, 考验jinija 的for循环和读inventory文件

取facts变量的方法参考balancer的模板:
/home/ansible/roles/balancer/roles/template/haproxy.cfg.j2

基于给的hosts.j2 文件修改, 拼凑出ip url alias格式,分别对应 default_ipv4.address, fqdn, hostname
ansible_facts在hostvars[host]中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 取
{% for host in groups.all %}
{{ hostvars[host]['ansible_facts'] *忽露不写了* }} × ×
{% endfor %}
---

创建playbook的要小心,题目要求生成所有主机的纪录,所以hosts是all, 但只部署在dev组的主机上要加when,
要加`when: 'dev' in group_names`

```yml
---
- name: gen hosts and deploy on dev group
tasks:
- name: gen hosts and deploy
template:
src: hosts.j2
dest: /etc/myhosts
when: "'dev' in group_names"

创建web server, 注意将文件标注setype为httpd_system_content_t

通过man semanager-fcontext 查询setype, 或者使用ls -ldZ来查询selinux标签

创建文件,目录、链接都用file模块,但操作文件内容用copy模块
创建组用group模块,创建用户用user模块
容易搞反的链接的src是指向的文件,所以是/webdev, 可以看帮助文档

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
---
- name: create web server
hosts: webservers
tasks:
- name: add group
group:
name: webdev
state: present
- name: create directory
file:
path: /webdev
group: webdev
state: directory
mode: u+rwx,g+rwxs,o-rx
- setype: httpd_system_content_t
- name: create a symbolic link
file:
- src: /var/www/html/webdev
- dest: /webdev
+ src: /webdev
+ dest: /var/www/html/webdev
state: link
- name: create index.html
copy:
content: Development
dest: /var/www/html/webdev/index.html
+ setype: httpd_system_content_t

创建硬件报告,用到loops进行数组循环,内置变量item和 regexs匹配,lineinfile 进行文件编辑

1
2
3
4
5
6
7
8
9
---
- name: generate hw report
hosts: all
vars:
hw_all:
- hw_name: HOST
hw_content: "{{ ansible_facts['hostname'] }}"
- hw_name: BIOS_version
hw_content: "{{ ansible_facts['bios_version'] }}"

创建加密文件,用到ansible-vault命令

1
ansible-vault encrypt lock.yml --vault-password-file=secret.txt

添加用户, ansible-playbook 通过 –vault-password-file=secret.txt 来传密码文件

通过vars_files来读取变量文件, loop 来循环遍历数组变量, 然后模板直接取密码后用’| password_hash(‘sha512’)’ 来加密密码
通常情况下在playbook中,开头位置引用一个变量时都需要”“, 但是在when块里,则直接使用 variable。 另外字符串要用“string”

创建用户时, 指定附属组用groups, 主要组是group

1
2
3
4
- {{ variable }} ## 错误
key: {{ variable }} ## 错误
- "{{ variable}}" ## 正确
key: "{{ variable }}" ## 正确

结果

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
---
- name: create user on dev,test
hosts: dev,test
vars_files:
- lock.yml
- user_list.yml
tasks:
- name: create group devops
group:
name: devops
state: present
- name: create user
user:
name: "{{ item.name }}"
- group: devops
+ groups: devops
password: "{{ pw_developer | password_hash('sha512') }}"
loop: "{{ user }}"
- when: "{{ item.job }}" == developer
+ when: item.job == "developer"
- name: create user on prod
hosts: prod
vars_files:
- lock.yml
- users_list.yml
tasks:
- name: create group manager
group:
name: manager
state: present
- name: create user
user:
name: "{{ item.name }}"
groups: manager
password: "{{ pw_manager | password_hash('sha512') }}"
loop: "{{ user }}"
- when: "{{ item.job }}" == manager
+ when: item.job == "manager"

修改密码, 使用ansible-vault rekey 修改密码,或者先解密后加密

修改文件内容, 使用copy模块,以及when条件判断

在playbook的内建变量 inventory_hostname 表示定义在inventory里的主机名称
也可以使用ansible_facts[‘hostname’],前提是被管理节点的hostname必须如果管理节点定义名称一样。 因为后者是到被管理节点获取。

使用when 限定条件, 满足条件才能执行这个task

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
---
- name: create issue
hosts: all
tasks:
- name: create issue file
copy:
content: "Development"
dest: /etc/issue
when: inventory_name in groups.dev
```

### 第二次排错

#### ansible-galaxy 安装角色, 指定方法分别是 src/name

```yml
- src: files/5/balancer.tar
name: balancer
- src: files/5/phpinfo.tar
name: phpinfo

timesync 看doc 示例, 验证方法是看timedatectl 看system clock syncronize 为 yes

1
2
3
4
5
6
7
8
9
---
- name: use timesync role
hosts: all
vars:
timesync_ntp_servers:
- hostname:
iburst: yes
roles:
- rhel_system_roles.timesync

创建hosts, 要用

使用firewalld模块时,需要指定immediate 参数,确保能立即生效

设置文件的权限时,应使用u=rwx,g=rwxs,o=rw, 而不是u+rwx,原因很好理解

sql operator priority

SQL优先级问题

例如数学中的加减乘除,以及编程里的运算符,在sql里也有优先级,依次是:

1
2
3
4
5
6
FROM clause
WHERE clause
SELECT clause
GROUP BY clause
HAVING clause
ORDER BY clause

优先级依次下降,所以FROM的优先级比WHREE高,这样会有什么影响?

优先级的问题

因为SELECT的优先级比WHERE低,所以在SELECT里面定义的alias别称就不能在WHERE 里面使用
但可以在ORDER里面使用,因为ORDER 后执行

常见问题

1
2
3
...
GROUP BY a, b, c
ORDER BY NULL
1
2
3
. . .
GROUP BY a, b, c
ORDER BY a, b, c

在上面的两个例子里的ORDER BY都不会执行,分别解释:

  1. 第一个例子里的ORDER 是删除GROUP的排序
  2. 第二个例子的ORDER是重复了GROUP已经干了的事情

验证方法

可以找个在线SQL工具,例如 http://sqlfiddle.com/

先执行创建数据库命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
DROP TABLE if exists new_table;

CREATE TABLE `new_table` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`testdecimal` decimal(6,2) DEFAULT NULL,
PRIMARY KEY (`id`));

INSERT INTO `new_table` (`testdecimal`) VALUES ('1234.45');
INSERT INTO `new_table` (`testdecimal`) VALUES ('1234.45');

set @mysqlorder := '';

select @mysqlorder := CONCAT(@mysqlorder," SELECT ") from new_table,(select @mysqlorder := CONCAT(@mysqlorder," FROM ")) tt
JOIN (select @mysqlorder := CONCAT(@mysqlorder," JOIN1 ")) t on ((select @mysqlorder := CONCAT(@mysqlorder," ON1 ")) or rand() < 1)
JOIN (select @mysqlorder := CONCAT(@mysqlorder," JOIN2 ")) t2 on ((select @mysqlorder := CONCAT(@mysqlorder," ON2 ")) or rand() < 1)
where ((select @mysqlorder := CONCAT(@mysqlorder," WHERE ")) or IF(new_table.testdecimal = 1234.45,true,false))
group by (select @mysqlorder := CONCAT(@mysqlorder," GROUPBY ")),id
having (select @mysqlorder := CONCAT(@mysqlorder," HAVING "))
order by (select @mysqlorder := CONCAT(@mysqlorder," ORDERBY "));

在执行 select @mysqlorder; 结果如下:

1
2
@mysqlor`der
(null)

可见其优先级顺序为

1
FROM JOIN1 JOIN2 WHERE ON2 ON1 ORDERBY GROUPBY SELECT WHERE ON2 ON1 ORDERBY GROUPBY SELECT HAVING HAVING

install k8s cluster

搭建k8s集群

引用 https://blog.piaoruiqing.com/2019/09/17/kubernetes-1-installation/
照着文章搭建k8s集群,写得挺好,记录一下自己的搭建方法和问题

环境

  1. 2个kvm虚拟机, 安装的centos7系统
  2. 两个虚拟机都配置了双网卡,eth0和eth1, eth0桥接宿主机的bridge, eth1 接入libvirt default NAT网络
  3. 官方推荐配置: CPU > 2,内存 > 2G
  4. eth1 的ip 分别为 192.168.122.61, 192.168.122.161

Master和Worker 共同步骤

需要在所有节点执行

修改hostname

编辑hostname

1
vim /etc/hostname # 修改hostname

配置host文件, 将所有节点都指定host

关闭防火墙

  1. systemctl disable –now firewalld
  2. 修改 /etc/seconfig, 将selinux 配置成disable

关闭 swap 分区

  1. swapoff -a
  2. 修改 /etc/fstab, 注释掉swap 记录

安装docker 配置镜像源

修改 /etc/docker/daemon.json 文件

1
2
3
4
5
6
7
8
9
{
"registry-mirrors": ["https://xxxxxxxx.mirror.aliyuncs.com"],
"exec-opts": ["native.cgroupdriver=systemd"],
"log-driver": "json-file",
"log-opts": {
"max-size": "100m"
},
"storage-driver": "overlay2"
}

添加k8s 软件源

如下添加的aliyun的镜像源

1
2
3
4
5
6
7
8
9
10
 cat <<EOF > /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=http://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=http://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg http://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg
exclude=kube*
EOF

安装k8s

安装组件

1
2
3
4
5
yum install -y kubelet kubeadm kubectl --disableexcludes=kubernetes
systemctl enable kubelet && systemctl start kubelet
```

### 配置网络

cat < /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
EOF
sysctl –system

1
2
3
4
5
6
7
8

上面的步骤需要在主备上执行

## Master 操作

下面的步骤只在Master节点运行

### 生成k8s初始化配置文件

[root@k8s-master ~]$ kubeadm config print init-defaults > kubeadm-init.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
得到 kubeadm-init.yaml 文件

修改kubeadm-init.yaml 文件
1. 修改 `advertiseAddress: 1.2.3.4` 为本机地址 192.168.122.61
2. 修改 `imageRepository: k8s.gcr.io ` 为 imageRepository: registry.cn-hangzhou.aliyuncs.com/google_containers

修改镜像地址,这样就避免下载k8s 镜像超时

修改后的kubeadm-init.yaml

```yaml
apiVersion: kubeadm.k8s.io/v1beta2
bootstrapTokens:
- groups:
- system:bootstrappers:kubeadm:default-node-token
token: abcdef.0123456789abcdef
ttl: 24h0m0s
usages:
- signing
- authentication
kind: InitConfiguration
localAPIEndpoint:
advertiseAddress: 10.33.30.92
bindPort: 6443
nodeRegistration:
criSocket: /var/run/dockershim.sock
name: k8s-master
taints:
- effect: NoSchedule
key: node-role.kubernetes.io/master
---
apiServer:
timeoutForControlPlane: 4m0s
apiVersion: kubeadm.k8s.io/v1beta2
certificatesDir: /etc/kubernetes/pki
clusterName: kubernetes
controllerManager: {}
dns:
type: CoreDNS
etcd:
local:
dataDir: /var/lib/etcd
imageRepository: registry.cn-hangzhou.aliyuncs.com/google_containers
kind: ClusterConfiguration
kubernetesVersion: v1.15.0
networking:
dnsDomain: cluster.local
serviceSubnet: 10.96.0.0/12
scheduler: {}

下载k8s的组件镜像

1
kubeadm config images pull --config kubeadm-init.yaml

初始化 k8s

1
kubeadm init --config kubeadm-init.yaml

执行无误后,得到worker 节点加入集群的命令

1
2
3
4
5
6
7
...
Your Kubernetes control-plane has initialized successfully!
...
Then you can join any number of worker nodes by running the following on each as root:

kubeadm join 10.33.30.92:6443 --token abcdef.0123456789abcdef \
--discovery-token-ca-cert-hash sha256:2883b1961db36593fb67ab5cd024f451b934fc0e72e2fa3858dda3ad3b225837

记录这个命令, 丢失的话也可以重新执行 kubeadmin create token 找回

配置普通用户执行kubectl 命令

用普通用户执行一下命令, 这样普通用户也能管理集群

1
2
3
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

查看Master 节点状态

可见,Master节点未就绪,这是因为还没安装网络组件

1
2
3
[root@k8s-master kubernetes]$ kubectl get node
NAME STATUS ROLES AGE VERSION
k8s-master NotReady master 3m25s v1.15.3

配置网络

  1. 下载calico 描述文件
    wget https://docs.projectcalico.org/v3.8/manifests/calico.yaml

  2. 修改calico 描述文件里的serviceSubnet,修改为跟kubeadmin-init.yaml一致

    1
    2
    cat kubeadm-init.yaml | grep serviceSubnet:
    serviceSubnet: 10.96.0.0/12

注意

  1. calico 的地址必须和 kubeadm-init.yaml 保持一致, kubeadm-init.yaml 默认为 serviceSubnet: 10.96.0.0/12, 而calico的默认地址为192.168.0.0/16, 所以要么修改kubeadmin-init.yaml, 要么修改calico.yaml。
  2. 这里的Subnet 不能涵盖到主机的地址范围即不能包含 192.168.122.0/24

安装calico

1
kubectl apply -f calico.yaml

查看Master节点

当calico 安装好了, 可以发现Master 节点变为就绪, 至此Master 节点就配置好了

1
2
3
[root@k8s-master ~]$ kubectl get node
NAME STATUS ROLES AGE VERSION
k8s-master Ready master 15m v1.15.3

安装dashboard

之前的步骤安装好了Master节点, 可选择安装dashboard,通过网页来管理集群

步骤很简单, 先下载dashboard, 再安装

1
2
[root@k8s-master ~]$ wget https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-beta4/aio/deploy/recommended.yaml
[root@k8s-master ~]$ kubectl apply -f recommended.yaml

创建dashboard 用户

必须创建用户,然后获取token,才能在集群外访问, 否则必须在Master 的localhost 访问dashboard。
以下是创建用户的描述文件, 注意namespace, 例如官方的例子是 dashboard, 这样生成的token 不一样

执行安装用户 kubectl apply -f dashboard-adminuser.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# dashboard-adminuser.yaml

apiVersion: v1
kind: ServiceAccount
metadata:
name: admin-user
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: admin-user
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: admin-user
namespace: kube-system

创建证书, k8s 不允许远程使用http,所以需要用证书访问dashboard

1
2
3
[root@k8s-master ~]$ grep 'client-certificate-data' ~/.kube/config | head -n 1 | awk '{print $2}' | base64 -d >> kubecfg.crt
[root@k8s-master ~]$ grep 'client-key-data' ~/.kube/config | head -n 1 | awk '{print $2}' | base64 -d >> kubecfg.key
[root@k8s-master ~]$ openssl pkcs12 -export -clcerts -inkey kubecfg.key -in kubecfg.crt -out kubecfg.p12 -name "kubernetes-client"

将生成的 kubecfg.p12 文件导入到集群外的主机,即kvm宿主机上

1
scp root@10.33.30.92:/root/.kube/kubecfg.p12 ./

在宿主机上使用chrome 的高级功能里可以导入证书

重启chrome 登录后登录

1
https://192.168.122.61:6443/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/#/login

打开网页后选择输入token, token通过在Master 节点执行一下命令生成, 注意参数 -n kube-system, 需要跟创建用户时的namespace保持一致

1
kubectl -n kube-system describe secret $(kubectl -n kube-system get secret | grep admin-user | awk '{print $1}')

复制token 到网页上,就能登录Dashboard

Worker节点加入集群

直接在worker节点执行

1
2
kubeadm join 10.33.30.92:6443 --token abcdef.0123456789abcdef \
--discovery-token-ca-cert-hash sha256:2883b1961db36593fb67ab5cd024f451b934fc0e72e2fa3858dda3ad3b225837

然后到Master 节点执行以下命令, 这里Name 显示了k8s-worker是因为我们在Hosts文件里指定了ip,所以k8s自动识别到了,否则会显示节点ip

1
2
3
4
[root@k8s-master ~]$ kubectl get node
NAME STATUS ROLES AGE VERSION
k8s-master Ready master 10h v1.15.3
k8s-worker Ready <none> 96s v1.15.3

至此集群搭建完毕

遇到问题

  1. 因为我的虚拟机配置的双网卡, 需要在calico.yaml 配网卡interface
    1
    2
    3
    4
    5
    # Cluster type to identify the deployment type
    - name: CLUSTER_TYPE
    value: "k8s,bgp"
    - name: IP_AUTODETECTION_METHOD
    value: "interface=eth1"

python wraper

decoder 介绍

python decoder(装饰器)是python的高级用法,用来拓展函数或类的功能。不过也增加初学者
阅读代码的难度

例如flask可以在需要验证用户登录的请求函数上添加装饰器,只有当用户登录了才可以调用该>页面,非常方便

原理

decoder 其实也是一个函数,装饰器函数会返回真实函数,并会增加额外的代码,如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 定义wraper
def log(func):
def wrapper(*arg):
print('func called')
return func(*arg)

return wrapper

# 使用
def hello(Name=None):
print('Hello %s'%Name)

new_hello = log(hello)

new_hello('bob')

输出
func called
Hello bob

使用wraper 的原因

提供额外的功能,例如计时,日志等。 另外wraper还有更多强大的功能

1
2
3
def fib(n):
if n <= 1: return 1
return fib(n-1) + fib(n-2)

这种递归调用非常费时,而使用wraper 可以提供一个缓存器

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
def memory(func):
cache = {}
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper

@memory
def fib(n):
if n <= 1: return 1
return fib(n-1) + fib(n-2)


## 给类wraper

上面是函数wraper, 类也可以作为wraper, 但需要类支持()调用,类似c++的类函数。
在python里需要实现 `__call__` 方法

```python
import functools

class MemoryClass():
def __init__(self, is_logging, func):
self.is_logging = is_logging
self.func = func
if self.is_logging:
print('logging')
self.cache = {}

def __call__(self, *args):
if args not in self.cache:
self.cache[args] = self.func(*args)

return self.cache[args]

def memory(is_logging):
return functools.partitial(MemoryClass, is_logging)


@memory(is_loging=True)
def fib(n):
if n <= 1: return 1
return fib(n-1) + fib(n-2)


print(fib(5))

类wrapr 可以增加参数,相比函数装饰器,类装饰器具有更加灵活,封装性更好等优点~

wraper 带来的问题

  1. 改变原来的函数属性,例如函数名, 因为装饰器用新函数替换了原来的旧函数。 所以新函>数少了旧函数的属性
    解决办法
1
2
3
4
5
6
7
8
9
10
11
12
13
import functools

def advertising(func):
@functools.wraps(func)
def wrapper(*args):
print('欢迎关注微信公众号: Charles的皮卡丘')
return func(*args)
return wrapper

@advertising
def add_1(a, b):
'''加法运算'''
return a + b
  1. 获取参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

import inspect
import functools

def check(func):
a = 0
@functools.wraps(func)
def wraper(*args, **kwargs):
a += 1
print(a)
# 在wraper里检查参数
getcallargs = inspect.getcallargs(func, *args, **kwargs)
if getcallargs.get('divisor') <= 1e-6 and getcallargs.get('divisor') >= -1e-6:
return False
return func(*args, **kwargs)

return wraper


@check
def dowork(arg1, arg2):
return arg1 + arg2

dowork(1,2)

此时会报错,a在赋值之前引用了。 需要将a声明为 nonlocal

Dockerfile 中的ENTRYPOINT和CMD的区别

区别

Dockerfile 中,ENTRYPOINT和CMD来都可以用来设置默认命令,而代替docker run时的命令,也可以一起使用

单独使用ENTRYPOINT或CMD时,其区别在于对docker run的参数处理:
CMD的命令会被docker run的参数覆盖,而ENTRYPOINT是追加

例如:

1
2
3
# image1
FROM ubuntu
ENTRYPOINT ["echo", "hello world"]
1
2
3
# image2
FROM ubuntu
CMD ["echo", "hello world"]

执行结果

1
2
3
4
5
# docker run image1
hello world

# docker run image1 hostname
0c33ff9af81c
1
2
3
4
5
# docker run image2
hello world

# docker run image2 hostname
hello world 0c33ff9af81c

实际情况多是entrypoint与cmd组合使用

cmd 为entrypoint 提供默认参数, 即如果’docker run’给了参数时,覆盖CMD参数,否则使用CMD提供的参数

1
2
3
FROM ubuntu
ENTRYPOINT ["top", "-b"]
CMD ["-c"]

首先说cmd和entrypoint 都有两种模式

  1. shell模式, 即没有用数组来传递参数, 直接接命令如 CMD top -p 或者 entrypoint top -p
  2. exec 模式, 即使用数组表示如 CMD [“top”, “-p”], 注意必须用双引号,因为这个是用json来传递的

两者区别:
shell模式不会接受命令行参数,而且1号进程是sh,而非如上的top。 这样的话top不能接受到docker stop container时的信号。
而一般我们写的程序都是捕捉SIGTERM来进行peaceful exit,那么就一定不能使用shell模式

exec模式时, 可以接受docker run image param, 这个param会传递给top作为参数。 而且top 是1号进程,能接收信号。

细微区别: 由于shell模式是先启动sh, 符号解析由shell执行,例如$HOME的展开由shell进行, 而用exec模式时,由docker 进行

总之,推荐使用exec 模式

额外话, 减小镜像体积

使用’' 或者 ‘&&’ 将命令连起来可以减小镜像层数,从而减小体积。另外build时,单独一行的命令会在新的layer 层执行。有些命令产生的cache,会持续影响到下一层
例如RUN apt-get dist-upgrade -y,如果不想保留cache,需要在build时加入参数docker build --no-cache.

还有最有效减小镜像体积的方式,也常见的是最后用空白镜像,’FROM SCRATCH’, 这样把其他的层都干掉了, 只保留一层镜像

create_loop_block

问题, 如果在文件系统之上再创一个文件系统?

例如在ext3的文件系统上创建一个xfs的文件系统,可以通过回环设备loop, 我们经常通过 mount -o loop 来 mount一个iso文件
但mount 的选项总是ro的

1
mount: /mnt: WARNING: device write-protected, mounted read-only.

不仅如此, 先在当前文件系统dd出一个文件, 再绑定到loop设备上,然后mount 到某个目录后, 可以进行读写访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[root@ha1 ~]# dd if=/dev/urandom of=file bs=1M count=2
2+0 records in
2+0 records out
2097152 bytes (2.1 MB, 2.0 MiB) copied, 0.0164121 s, 128 MB/s

[root@ha1 ~]# mkfs.ext3 file
mke2fs 1.44.6 (5-Mar-2019)
Discarding device blocks: done
Creating filesystem with 2048 1k blocks and 256 inodes

Allocating group tables: done
Writing inode tables: done
Creating journal (1024 blocks): done
Writing superblocks and filesystem accounting information: done

[root@ha1 ~]# mount -o loop file /mnt
[root@ha1 ~]# ls /mnt/
lost+found

[root@ha1 mnt]# echo abc > abc
[root@ha1 mnt]# ls
abc lost+found

绑定loop设备和挂载是由mount 一个命令完成的。

手动绑定loop

1
2
3
4
[root@ha1 ~]# losetup -f
/dev/loop1

[root@ha1 ~]# losetup -f file

这样只是将文件绑定了loop设备,需要再挂载到文件目录mount /dev/loop0 /mnt

1
2
3
[root@ha1 ~]# ls /mnt/
abc lost+found
[root@ha1 ~]#

losetup -f 可以返回第一个未被使用的loop设备名

创建loop设备

有的系统默认创建了 loop0 .. loop7 的块设备,有的则是在需要的时候创建,比如mount iso的时候发现没有loop设备,则会创建

  1. 手动创建loop设备通过 mknode 创建
1
2
3
4
5
mknode  /dev/loop0 b 7 0
mknode /dev/loop1 b 7 0
...

mknode /dev/loop7 b 7 0
  1. 如果8个loop0 .. loop8 设备都占用了, 可以再创建loop8
1
2
3
4
$sudo mknod /dev/loop8 b 7 8 

$ls -l /dev/loop8
brw-r--r-- 1 root root 7, 8 Jun 11 19:16 /dev/loop8

ps: /dev/loop* 是块设备

bash-safe

bash 脚本安全

不管是大厂还是小厂,总能听到一些误操作新闻
那避免误操作就办法就是写脚本, 然后也有很多脚本出事的新闻, 通常有以下

引用了不存在的变量

1
2
3
4
5
#!/bin/bash

ABC_PATH=abc

/bin/rm ${ABC_PATH}/

如果ABC_PATH变量被unset了, 那么就是删除系统了
不过听说/bin/rm / 的操作会可能被系统禁止, 但是/bin/rm /*则肯定能执行成功

脚本没有输出

有很多公司用堡垒机来审计操作记录, 但是如果脚本没有输出,那么就不知道执行了什么命令, 再如果脚本也被删除死无对证,那就无法分析了

解决办法

在bash 脚本前面加上 set -xeu

-x 调试模式,bash会输出所有命令
-e 遇到错误就退出
-u 引用了不存在的变量就退出

添加默认值

path=${ABC_PATH-/} #default /

这样,当ABC_PATH不存在时,就使用默认值’/‘

最好的办法

使用ansible, 可以对脚本进行版本管理和review, 可以避免脚本难以维护的问题
使用ide + bash 插件, 可以有提示

良好的脚本习惯

除了在脚本前面加上 set -xeu外, 有一些建议

  1. 多用&&,|| ,少用 ; ,如果前面的命令出错,后面就不会执行, 而; 不管
  2. 善用 date, 例如将日志文件加上日期 bash test.sh > log_date +%m_%d_%H_%M_%S``
  3. 脚本放在指定目录,不要与工作目录放在一起, 避免脚本自己被删, 死无对证
  4. 定期备份

bash 的条件判断

有以下脚本,当TMP 不为空的时候,才进行判断值是否大于2

1
2
3
4
5
6
7
8
9
10
if [ -n "$TMP" -a ${TMP} -gt 2 ];then
echo "aaaa"
else
echo "bbbbbbbb"
fi
```

而当TMP 为空的时候,就会出现 `too many arguement`。这就说明了bash的执行流程,是先进行整行替换再执行(起码是整行,也许还是全文替换)。
替换后`[-n "" -a -gt 2]`。导致了报错。
解决这样的问题可以用默认值 TMP=${TMP:=2}, 这样当TMP为空的时候就会自动赋值为2

if [ -n “$TMP” -a ${TMP:=2} -gt 2 ];then
echo “aaaa”
else
echo “bbbbbbbb”
fi

register

register 关键字

register 关键字是c/c++ 里面唯一能操作寄存器的指令,强制将数据存放在寄存器里,而不是放在堆栈里,这样读写速度最快。

而汇编语言强大之处是能直接操作寄存器,且可以指定使用哪些寄存器。

通过汇编一段代码来演示register的作用

先是c语言代码

1
2
3
4
5
6
7
8
9
int main(){

register int i;

for(i = 0 ;i<10;i++){
int j = i;
}

}

对应的汇编

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
pushq %rbx
.cfi_offset 3, -24
movl $0, %ebx
jmp .L2
.L3:
movl %ebx, -12(%rbp)
addl $1, %ebx
.L2:
cmpl $9, %ebx
jle .L3
movl $0, %eax
movq -8(%rbp), %rbx
leave
.cfi_def_cfa 7, 8
ret

没有register关键字的时候

1
2
3
4
5
6
7
8
9
int main(){

int i;

for(i = 0 ;i<10;i++){
int j = i;
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $0, -8(%rbp)
jmp .L2
.L3:
movl -8(%rbp), %eax
movl %eax, -4(%rbp)
addl $1, -8(%rbp)
.L2:
cmpl $9, -8(%rbp)
jle .L3
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret

可见,当register修饰int i时, i的值存放在%ebx里,当没有register修饰时,i的值放在栈里%rbp -8的位置
movl %ebx, -12(%rbp) 可见当register时, j的值复用了ebx的高32位。 此时的内存如下

1
2
3
4
5
6
7
--- 栈底
旧的rbp 64位
---
旧的rbx 高32位
--- j
旧的rbx 低32位
---栈顶

rbp表示基地址指针,指向的是函数栈的起始地址,这个起始地址只是对于当前函数而言的
rsp表示栈顶指针,指向当前的栈顶
每次函数调用时(main 函数被内核_start_main__调用):

  1. 将调用者的函数栈基地址保存起来
  2. 参数压栈, 可能由调用者压栈,可能由被调用者压栈,影响的是当前的栈顶,继而栈顶影响rbp
  3. 将被调用函数当前的栈顶复制给基地址寄存器。新的rbp == rsp即当前的栈顶。相当于构造了新的函数运行环境

附录

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
General-Purpose Registers

The 64-bit versions of the 'original' x86 registers are named:

rax - register a extended
rbx - register b extended
rcx - register c extended
rdx - register d extended
rbp - register base pointer (start of stack)
rsp - register stack pointer (current location in stack, growing downwards)
rsi - register source index (source for data copies)
rdi - register destination index (destination for data copies)

The registers added for 64-bit mode are named:

r8 - register 8
r9 - register 9
r10 - register 10
r11 - register 11
r12 - register 12
r13 - register 13
r14 - register 14
r15 - register 15

These may be accessed as:

64-bit registers using the 'r' prefix: rax, r15
32-bit registers using the 'e' prefix (original registers: e_x) or 'd' suffix (added registers: r__d): eax, r15d
16-bit registers using no prefix (original registers: _x) or a 'w' suffix (added registers: r__w): ax, r15w
8-bit registers using 'h' ("high byte" of 16 bits) suffix (original registers - bits 8-15: _h): ah, bh
8-bit registers using 'l' ("low byte" of 16 bits) suffix (original registers - bits 0-7: _l) or 'b' suffix (added registers: r__b): al, bl, r15b

Usage during syscall/function call:

First six arguments are in rdi, rsi, rdx, rcx, r8d, r9d; remaining arguments are on the stack.
For syscalls, the syscall number is in rax.
Return value is in rax.
The called routine is expected to preserve rsp,rbp, rbx, r12, r13, r14, and r15 but may trample any other registers.

pessimistic_lock_optimistic_lock

乐观锁悲观锁

先说这两种锁都不是编程里面用到的锁, 而是一种策略。 在数据库中用的多。
而且在java里面有个原子bool AtomicBoolean 的实现也用到乐观锁的策略, AtomicBoolean::compareandswap

这里以数据库管理服务(DBMS)来说明这两种锁的区别,在并发的情况下

悲观锁

悲观锁策略预期是多个请求会操作同一个数据,导致数据不可靠。 悲观锁的策略就是上锁,不让其它请求修改, 即排他锁。

这种排他锁与我们平常编程用的互斥锁道理相同, 是性能比较低的一种锁,但能保证数据准确。

乐观锁

乐观锁实际是不上锁, 预期多个请求不会修改同一分数据。而是在最后提交的时候再去确认数据是否一致,如果一致则提交,如果不一致则报错,需要认为干涉

所以乐观锁的数据也是可靠的, 且性能较好。 这里关键是如何确认数据一致

乐观锁的确认并提交

这里举例sql的来说明

1
2
3
4
5
select quanity from items where id =1;

//得到quanity=3,然后更新

update items set quantity=2 where id=2 and quantity=3

更新时,会确认quantity为预期的3,否则不执行或者报错。

这样有个问题, 虽然单条语句是原子的,但是两条语句不是, 会出现ABA的情况

例如当一个请求1执行到select后,得知quantity=3, 然后请求2先更新quantity=2,然后又更新为quantity=3
然后请求1继续执行update,这会成功,虽然quantity=3是一致的,但是不能保证其它数据是一致的或者完全没有引起安全问题

因此另一种办法是判断只增version或者timestamp,每次请求时去更新。这两种办法都能确保数据一致。
但是因为数据库同时只允许一个请求修改version,那么就出现了竞争情况,会导致其它请求失败

例如多个请求同时记录了version=1, 则当一个请求完成的时候,version=2, 那么其它请求提交时发现verison变了,只能报告失败
虽然这样能保证数据正确,但是会导致其它请求失败

而电商平台中, 则采用另一种方式确认

1
2
3
4
5
select quanity from items where id =1;

//得到quanity=3,然后更新

update items set quantity=2 where id=2 and quantity > 0

这样就规避了上面两个方法的缺点

compareandswap

1
compareAndSet(expect_val, new_val)

上面的乐观锁就是compareandswap的解释:

先对比,如果一致则更新

再说一下java的实现,

1
2
3
4
5
6
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))  
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

在unsafe.cpp 找到实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
#define LOCK_IF_MP(mp) __asm cmp mp, 0 \
__asm je L0 \
__asm _emit 0xF0 \
__asm L0:

int mp = os::is_MP();
__asm {
mov edx, dest
mov ecx, exchange_value
mov eax, compare_value
LOCK_IF_MP(mp)
cmpxchg dword ptr [edx], ecx
}

_emit 是用于生成指令的, 生成锁的指令,有cpu提供锁机制
从上下文看, 这个指令应该是上锁, 如果mp != 0 则上锁。

nginx fails to connect to unix socket

记录在部署一个nginx fastcgi 项目时失败, fastcgi 通过unix socket 通讯

这是一个flask 项目, 使用flup创建的fastcgi

1
2
3
4
5
6
7
8
9
#!/usr/bin/env python
from flup.server.fcgi import WSGIServer
from main import app

if __name__ == '__main__':
# for apache
# WSGIServer(app).run()
# for nginx, need to set conmunication sock between nginx and the cgiserver
WSGIServer(app, bindAddress='/tmp/myflask-fcgi.sock').run()

这里直接说明结果, socket 文件不能创建在/tmp/ 或者 /vat/tmp 目录。

nginx 错误配置

1
2
3
4
5
6
7
8
9
10
    location / { try_files $uri @myflask; }
location @myflask {
include fastcgi_params;
fastcgi_param PATH_INFO $fastcgi_script_name;
fastcgi_param SCRIPT_NAME "";
fastcgi_pass unix:/tmp/fcgi.sock;
}
```

nginx 错误提示

connect() to unix:/tmp/fcgi.sock failed (2: No such file or directory) while connecting to upstream

1
2
3
4
5
6
7
8



## 原因

如果socket创建在/tmp 或者 /var/tmp 目录, 只能被socket的进程发现。 意思是只有cgi程序能发现,所以nginx 无法连接了

正确配置是创建到/var/sockets 目录,并注意权限问题
location / { try_files $uri @myflask; }
location @myflask {
    include fastcgi_params;
    fastcgi_param PATH_INFO $fastcgi_script_name;
    fastcgi_param SCRIPT_NAME "";
    fastcgi_pass unix:/var/sockets/myflask-fcgi.sock;
}
1
2
3
4
5
6
7
8
9

## 关于unix socket

以上是一种解决办法,当然使用ip来连接nginx和cgi也是可以的,但是unix socket 有不少好处。

## 关于cgi

上面无论使用unix socket 还是 ip 来访问cgi, 这两种方式都是通过cgi来提供web服务。
而nginx 不支持cgi, 所以需要自己来通过python的 flup 来实现fastcgi。 nginx 只不过是一个代理的作用。而apache和tomcat 都能支持cgi

cgi 的定义是 common gateway interface, 是一个协议。 fastcgi,uwcgi都是升级版或者变种。
flup实现了cgi的服务程序,也就是可以用来解析脚本(python php 等等),提供web服务。

1
2
3


另外除了以上方式, nginx也可以使用pypass proxy 来转发请求,由应用自己来实现网络处理。

location / {
proxy_pass https://localhost:8080
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
access_log /var/log/nginx/access.log my_tracking;
}


如过是python 就需要监听某个端口,这种是不稳定的。 不适合生产模式下使用。