MySQL运维系列 之 如何快速定位IO瓶颈

摘要: MySQL的瓶颈,一般分为IO密集型和CPU密集型 CPU出问题的情况比较少,最近就遇到过一次比较大的故障,这个话题后面会有一篇专题介绍 今天主要聊聊IO密集型的应用中,我们应该如何快速定位到是谁占用了IO资源比较多 背景 环境1.

MySQL的瓶颈,一般分为IO密集型和CPU密集型

CPU出问题的情况比较少,最近就遇到过一次比较大的故障,这个话题后面会有一篇专题介绍

今天主要聊聊IO密集型的应用中,我们应该如何快速定位到是谁占用了IO资源比较多

背景

  • 环境
1. MySQL 5.7 +
    低版本MySQL这边不再考虑,就像还有使用SAS盘的公司一样,费时费力,MySQL5.7+ 标配
2. InnoDB 存储引擎
3. Centos 6

实战

关于IO的问题,大家能想到的监控工具有哪些

  • iostat
  • dstat
  • iotop

没错,以上都是神器,可以直接用iotop找到占用资源最多的进程

先上一张图

未分类

是的,根据这张图,你能发现的就是MySQL的某个io线程占用了比较多的disk资源,然后呢?

然后,就是去MySQL里面去找,有经验的DBA会去看slow log,或者processlist中去查找相关的sql语句

通常情况下,DBA只会一脸茫然的看到一堆MySQL的query语句,一堆slow log里面去分析,有如大海捞针,定位问题繁琐而低效

如果,你使用的是MySQL5.7+ 版本,那么你就会拥有一件神器(说了好多遍了),可以快速而精准的定位问题

  • 如何快速定位到IO瓶颈消耗在哪里

iotop + threads

dba:lc> select * from performance_schema.threads where thread_os_id=37012G
*************************** 1. row ***************************
          THREAD_ID: 96
               NAME: thread/sql/one_connection
               TYPE: FOREGROUND
     PROCESSLIST_ID: 15
   PROCESSLIST_USER: dba
   PROCESSLIST_HOST: NULL
     PROCESSLIST_DB: sbtest
PROCESSLIST_COMMAND: Query
   PROCESSLIST_TIME: 0
  PROCESSLIST_STATE: query end
   PROCESSLIST_INFO: INSERT INTO sbtest1(k, c, pad) VALUES(25079106, '33858784348-81663287461-16031064329-06006952037-79426243027-69964324491-90950423034-40185804987-62166137368-06259615216', '47186118229-42754
696460-81034599900-41836403072-66805611739'),(24907169, '77074724245-16833049423-38868029911-54850236074-63700733526-39699866447-52646750572-85552352492-59476301007-32196580154', '79013412600-99031855741-696987
96712-65630963686-19653514942'),(24896311, '28403978193-66350947863-03931166713-97714847962-65299790981-39948912629-14070597101-63277652140-34421148430-61801121402', '05239379274-22840441238-37771744512-9234774
1972-52847679847'),(18489383, '89292717216-01584483614-67433536730-45584233994-29817613740-77179131661-10692787267-83942773303-14971155500-36206705010', '55201342831-85536327239-84383935287-06948377235-96437333
726'),(24790463, '99362943588-41160434740-62783664419-16002619743-04761662097-94273988379-52564232648-19738707042-79143532768-89687113917', '09717575620-89781830996-88443720661-19001024583-14971953687'),(2
   PARENT_THREAD_ID: NULL
               ROLE: NULL
       INSTRUMENTED: YES
            HISTORY: YES
    CONNECTION_TYPE: Socket
       THREAD_OS_ID: 37012
1 row in set (0.00 sec)

你看,消耗资源的SQL语句立刻就呈现在你眼前,就是如此高效

好了,以上列出的,还只是全部功能的冰山一角,更多的玩法等待你去解锁。

以上定位的问题也比较的简单,还有一些复杂的IO问题,比如:binlog写入过大、binlog扫描过多、同步线程阻塞、临时表造成的IO过大,等等问题,都可以用此神器一窥究竟

总结

  1. MySQL5.7 默默的提供了非常多的实用工具和新特性,需要DBA们去挖掘和探索。将看似平淡无奇的特性挖掘成黑武器,你才能成为那闪着光芒的Top5 MySQLer

  2. 工欲善其事必先利其器

MySQL root密码忘记,原来还有更优雅的解法!

一直以来,对于MySQL root密码的忘记,以为只有一种解法-skip-grant-tables。

问了下群里的大咖,第一反应也是skip-grant-tables。通过搜索引擎简单搜索了下,无论是百度,抑或Google,只要是用中文搜索,首页都是这种解法。可见这种解法在某种程度上已经占据了使用者的心智。下面具体来看看。

skip-grant-tables的解法

首先,关闭实例

这里,只能通过kill mysqld进程的方式。

注意:不是mysqld_safe进程,也切忌使用kill -9。

# ps -ef |grep mysqld
root      6220  6171  0 08:14 pts/0    00:00:00 /bin/sh bin/mysqld_safe --defaults-file=my.cnf
mysql      6347  6220  0 08:14 pts/0    00:00:01 /usr/local/mysql57/bin/mysqld --defaults-file=my.cnf --basedir=/usr/local/mysql57 --datadir=/usr/local/mysql57/data --plugin-dir=/usr/local/mysql57/lib/plugin --user=mysql --log-error=slowtech.err --pid-file=slowtech.pid --socket=/usr/local/mysql57/data/mysql.sock --port=3307
root      6418  6171  0 08:17 pts/0    00:00:00 grep --color=auto mysqld

# kill 6347

使用–skip-grant-tables参数,重启实例

# bin/mysqld_safe --defaults-file=my.cnf --skip-grant-tables  --skip-networking &

设置了该参数,则实例在启动过程中会跳过权限表的加载,这就意味着任何用户都能登录进来,并进行任何操作,相当不安全。

建议同时添加–skip-networking参数。其会让实例关闭监听端口,自然也就无法建立TCP连接,而只能通过本地socket进行连接。

MySQL8.0就是这么做的,在设置了–skip-grant-tables参数的同时会自动开启–skip-networking。

修改密码

# mysql -S /usr/local/mysql57/data/mysql.sock

mysql> update mysql.user set authentication_string=password('123456') where host='localhost' and user='root';
Query OK, 0 rows affected, 1 warning (0.00 sec)
Rows matched: 1  Changed: 0  Warnings: 1

mysql> flush privileges;
Query OK, 0 rows affected (0.00 sec)

注意:
这里的update语句针对的是MySQL 5.7的操作,如果是在5.6版本,修改的应该是password字段,而不是authentication_string。

update mysql.user set password=password('123456') where host='localhost' and user='root';

而在MySQL 8.0.11版本中,这种方式基本不可行,因为其已移除了PASSWORD()函数及不再支持SET PASSWORD … = PASSWORD (‘auth_string’)语法。

不难发现,这种方式的可移植性实在太差,三个不同的版本,就先后经历了列名的改变,及命令的不可用。

下面,介绍另外一种更通用的做法,还是在skip-grant-tables的基础上。

与上面不同的是,其会先通过flush privileges操作触发权限表的加载,再使用alter user语句修改root用户的密码,如:

# bin/mysql -S /usr/local/mysql57/data/mysql.sock

mysql> alter user 'root'@'localhost' identified by '123';
ERROR 1290 (HY000): The MySQL server is running with the --skip-grant-tables option so it cannot execute this statement

mysql> flush privileges;
Query OK, 0 rows affected (0.00 sec)

mysql> alter user 'root'@'localhost' identified by '123';
Query OK, 0 rows affected (0.00 sec)

免密码登录进来后,直接执行alter user操作是不行的,因为此时的权限表还没加载。可先通过flush privileges操作触发权限表的加载,再执行alter user操作。
需要注意的是,通过alter user修改密码只适用于MySQL5.7和8.0,如果是MySQL 5.6,此处可写成

update mysql.user set password=password('123456') where host='localhost' and user='root';

最后重启实例

mysql> shutdown;

# bin/mysqld_safe --defaults-file=my.cnf &

需要注意的是,如果在启动的过程中没有指定–skip-networking参数,无需重启实例。但在网上看到的绝大多数方案,都是没有指定该参数,但重启了实例,实在没有必要。

下面对这个方案做个总结:

  1. 如果只添加了–skip-grant-tables,修改完密码后,其实无需重启,执行flush privileges即可。

  2. 从安全角度出发,建议加上–skip-networking。但因其是静态参数,将其剔除掉需要重启实例。

  3. 加上–skip-networking,虽然可以屏蔽掉TCP连接,但对于本地其它用户,只要有socket文件的可读权限,都能无密码登录。还是存在安全隐患。

  4. 不建议通过update的方式修改密码,更通用的其实是alter user。

更优雅的解法

相对于skip-grant-tables方案,我们来看看另外一种更优雅的解法,其只会重启一次,且基本上不存在安全隐患。

首先,依旧是关闭实例

其次,创建一个sql文件

写上密码修改语句

# vim init.sql 
alter user 'root'@'localhost' identified by '123456';

最后,使用–init-file参数,启动实例

# bin/mysqld_safe --defaults-file=my.cnf --init-file=/usr/local/mysql57/init.sql &

实例启动成功后,密码即修改完毕~

如果mysql实例是通过服务脚本来管理的,除了创建sql文件,整个操作可简化为一步。

# service mysqld restart --init-file=/usr/local/mysql57/init.sql 

注意:该操作只适用于/etc/init.d/mysqld这种服务管理方式,不适用于RHEL 7新推出的systemd。

MySQL 内核深度优化

MYSQL数据库适用场景广泛,相较于Oracle、DB2性价比更高,Web网站、日志系统、数据仓库等场景都有MYSQL用武之地,但是也存在对于事务性支持不太好(MySQL 5.5版本开始默认引擎才是InnoDB事务型)、存在多个分支、读写效率瓶颈等问题。

所以如何用好MYSQL变得至关重要,一方面需要通过MYSQL优化找出系统读写瓶颈,提高数据库性能;另一方面需要合理涉及数据结构、调整参数,以提高用户操作响应;同时还有尽可能节省系统资源,以便系统可以提供更大负荷的服务。本文将为大家介绍腾讯云团队是如何对Mysql进行内核级优化的思路和经验。

早期的CDB主要基于开源的Oracle MySQL分支,侧重于优化运维和运营的OSS系统。在腾讯云,因为用户数的不断增加,对CDB for MySQL提出越来越高的要求,腾讯云CDB团队针对用户的需求和业界发展的技术趋势,对CDB for MySQL分支进行深度的定制优化。优化重点围绕内核性能、内核功能和外围OSS系统三个维度展开,具体的做法如下:

一.内核性能的优化

由于腾讯云上的DB基本都需要跨园区灾备的特性,因此CDB for MySQL的优化主要针对主从DB部署在跨园区网络拓扑的前提下,重点去解决真实部署环境下的性能难题。经过分析和调研,我们将优化的思路归纳为:“消除冗余I/O、缩短I/O路径和避免大锁竞争”。以下是内核性能的部分案例:

1.主备DB间的复制优化

未分类

问题分析

如上图所示,在原生MySQL的复制架构中,Master侧通过Dump线程不断发送Binlog事件给Slave的I/O线程,Slave的I/O线程在接受到Binlog事件后,有两个主要的动作:

  • 写入到Relay Log中,这个过程会和Slave SQL线程争抢保护Relay Log的锁。
  • 更新复制元数据(包含Master的位置等信息)。

优化方法

经过分析,我们的优化策略是:

  • Slave I/O线程和Slave SQL线程是典型的单写单读生产者-消费者模型,是可以做到无锁设计的;因此实现思路就是SlaveI/O线程在每次写完数据后,原子更新Relay Log的长度信息,Slave SQL线程读取RelayLog的时以长度信息为边界。这样就将原本竞争激烈的Relay Log锁化解为无锁;
  • 由于Binlog事件中的GTID(Global Transaction Identifier)和DB事务是一一对应的关系,所以RelayLog中的数据本身已经包含了所需要的复制元数据,所以我们可以不写Master info文件,消除了冗余的文件I/O;
  • 于DB都是以事务为更新粒度的,因为在RelayLog文件I/O上,我们通过合并离散小I/O为事务粒度的大I/O等手段,使磁盘I/O得以大幅提升。

优化效果

未分类

如上图所示,经过优化:左图35.79%的锁竞争(futex)已经被完全消除;同压测压力下,56.15%的文件I/O开销被优化到19.16%,Slave I/O线程被优化为预期的I/O密集型线程。

2.主库事务线程和Dump线程间的优化

未分类

问题分析

如上图所示,在原生MySQL中多个事务提交线程TrxN和多个Dump线程之间会同时竞争Binlog文件资源的保护锁,多个事务提交线程对Binlog执行写入,多个Dump线程从Binlog文件读取数据并发送给Slave。所有的线程之间是串行执行的!

优化方法

经过分析,我们的优化策略是:

  • 将读写分离开来,多个写入的线程还是在锁保护下串行执行,每一个写入线程写入完成后更新当前Binlog的长度信息,多个Dump线程以Binlog文件的长度信息为读取边界,多个Dump线程之间并行执行。以这种方式来让复制拓扑中的Dump线程发送得更快!

效果

未分类

经过测试,优化后的内核,不仅提升了事务提交线程的性能,在Dump线程较多的情况下,对主从复制性能有较大提升。

二.主备库交互流程优化

未分类

问题分析

如上图所示,在原生MySQL中主备库之间的数据发送和ACK回应是简单的串行执行,在上一个事件ACK回应到达之前,不允许继续发送下一个事件;这个行为在跨园区(RTT 2-3ms)的情况性能非常差,而且也不能很好地利用带宽优势。

优化方法

经过分析,我们的优化策略是:

  • 将发送和ACK回应的接收独立到不同的线程中,由于发送和接收都是基于TCP流的传输,所以时序性是有保障的;这样发送线程可以在未收ACK之前继续发送,接受线程收到ACK后唤醒等待的线程执行相应的任务。

效果

根据实际用例测试,优化后的TPS提升为15%左右。

三.内核功能的优化

1. 预留运维帐号连接数配额

在腾讯云上,不时遇到用户APP异常或者BUG从而占满DB的最大连接限制,这是CDB OSS帐号无法登录以进行紧急的运维操作。针对这个现状,我们在MySQL内核单独开辟了一个可配置的连接数配额,即便在上述场景下,运维帐号仍然可以连接到DB进行紧急的运维操作。极大地降低了异常情况下DB无政府状态的风险。该帐号仅有数据库运维管理权限,无法获取用户数据,也保证了用户数据的安全性。

2. 主备强同步

针对一些应用对数据的一致性要求非常高,CDB在MySQL原生半同步的基础上进行了深度优化,确保一个事务在主库上提交之前一定已经复制到至少一个备库上。确保主库宕机时数据的一致性。

四.外围系统的优化

除了以上提到的MySQL内核侧的部分优化,我们也在外围OSS平台进行了多处优化。例如使用异步MySQL ping协议实现大量实例的监控、通过分布式技术来加固原有系统的HA/服务发现和自动扩容等功能、在数据安全/故障切换和快速恢复方面也进行了多处优化。

在此我向大家推荐一个架构学习交流群。交流学习群号:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

mysql的UNIX_TIMESTAMP用法

mysql的UNIX_TIMESTAMP用法

一、UNIX_TIMESTAMP 一般是用于unix的时间戳。

例子一、日期转化为时间戳

SELECT UNIX_TIMESTAMP("2016-07-11")

结果:
— 1468166400

例子二、日期转化为时间戳

SELECT UNIX_TIMESTAMP("2016-07-17 23:59:59")

结果:

— 1468771199

二、FROM_UNIXTIME:表示把UNIX_TIMESTAMP还原成标准的时间格式

SELECT FROM_UNIXTIME(1468166400),FROM_UNIXTIME(1468771199)

结果:

2016-07-11 00:00:00 2016-07-17 23:59:59

记住,永远不要在MySQL中使用“utf8”

最近我遇到了一个bug,我试着通过Rails在以“utf8”编码的MariaDB中保存一个UTF-8字符串,然后出现了一个离奇的错误:

Incorrect string value: ‘xF0x9Fx98x83 <…’ for column ‘summary’ at row 1

我用的是UTF-8编码的客户端,服务器也是UTF-8编码的,数据库也是,就连要保存的这个字符串“ <…”也是合法的UTF-8。

问题的症结在于,MySQL的“utf8”实际上不是真正的UTF-8。

“utf8”只支持每个字符最多三个字节,而真正的UTF-8是每个字符最多四个字节。

MySQL一直没有修复这个bug,他们在2010年发布了一个叫作“utf8mb4”的字符集,绕过了这个问题。

当然,他们并没有对新的字符集广而告之(可能是因为这个bug让他们觉得很尴尬),以致于现在网络上仍然在建议开发者使用“utf8”,但这些建议都是错误的。

简单概括如下:

  • MySQL的“utf8mb4”是真正的“UTF-8”。
  • MySQL的“utf8”是一种“专属的编码”,它能够编码的Unicode字符并不多。

我要在这里澄清一下:所有在使用“utf8”的MySQL和MariaDB用户都应该改用“utf8mb4”,永远都不要再使用“utf8”。

那么什么是编码?什么是UTF-8?

我们都知道,计算机使用0和1来存储文本。比如字符“C”被存成“01000011”,那么计算机在显示这个字符时需要经过两个步骤:

  • 计算机读取“01000011”,得到数字67,因为67被编码成“01000011”。
  • 计算机在Unicode字符集中查找67,找到了“C”。

同样的:

  • 我的电脑将“C”映射成Unicode字符集中的67。
  • 我的电脑将67编码成“01000011”,并发送给Web服务器。

几乎所有的网络应用都使用了Unicode字符集,因为没有理由使用其他字符集。

Unicode字符集包含了上百万个字符。最简单的编码是UTF-32,每个字符使用32位。这样做最简单,因为一直以来,计算机将32位视为数字,而计算机最在行的就是处理数字。但问题是,这样太浪费空间了。

UTF-8可以节省空间,在UTF-8中,字符“C”只需要8位,一些不常用的字符,比如“”需要32位。其他的字符可能使用16位或24位。一篇类似本文这样的文章,如果使用UTF-8编码,占用的空间只有UTF-32的四分之一左右。

MySQL的“utf8”字符集与其他程序不兼容,它所谓的“”,可能真的是一坨……

MySQL简史

为什么MySQL开发者会让“utf8”失效?我们或许可以从提交日志中寻找答案。

MySQL从4.1版本开始支持UTF-8,也就是2003年,而今天使用的UTF-8标准(RFC 3629)是随后才出现的。

旧版的UTF-8标准(RFC 2279)最多支持每个字符6个字节。2002年3月28日,MySQL开发者在第一个MySQL 4.1预览版中使用了RFC 2279。

同年9月,他们对MySQL源代码进行了一次调整:“UTF8现在最多只支持3个字节的序列”。

是谁提交了这些代码?他为什么要这样做?这个问题不得而知。在迁移到Git后(MySQL最开始使用的是BitKeeper),MySQL代码库中的很多提交者的名字都丢失了。2003年9月的邮件列表中也找不到可以解释这一变更的线索。

不过我可以试着猜测一下。

2002年,MySQL做出了一个决定:如果用户可以保证数据表的每一行都使用相同的字节数,那么MySQL就可以在性能方面来一个大提升。为此,用户需要将文本列定义为“CHAR”,每个“CHAR”列总是拥有相同数量的字符。如果插入的字符少于定义的数量,MySQL就会在后面填充空格,如果插入的字符超过了定义的数量,后面超出部分会被截断。

MySQL开发者在最开始尝试UTF-8时使用了每个字符6个字节,CHAR(1)使用6个字节,CHAR(2)使用12个字节,并以此类推。

应该说,他们最初的行为才是正确的,可惜这一版本一直没有发布。但是文档上却这么写了,而且广为流传,所有了解UTF-8的人都认同文档里写的东西。

不过很显然,MySQL开发者或厂商担心会有用户做这两件事:

  • 使用CHAR定义列(在现在看来,CHAR已经是老古董了,但在那时,在MySQL中使用CHAR会更快,不过从2005年以后就不是这样子了)。
  • 将CHAR列的编码设置为“utf8”。

我的猜测是MySQL开发者本来想帮助那些希望在空间和速度上双赢的用户,但他们搞砸了“utf8”编码。

所以结果就是没有赢家。那些希望在空间和速度上双赢的用户,当他们在使用“utf8”的CHAR列时,实际上使用的空间比预期的更大,速度也比预期的慢。而想要正确性的用户,当他们使用“utf8”编码时,却无法保存像“”这样的字符。

在这个不合法的字符集发布了之后,MySQL就无法修复它,因为这样需要要求所有用户重新构建他们的数据库。最终,MySQL在2010年重新发布了“utf8mb4”来支持真正的UTF-8。

为什么这件事情会让人如此抓狂

因为这个问题,我整整抓狂了一个礼拜。我被“utf8”愚弄了,花了很多时间才找到这个bug。但我一定不是唯一的一个,网络上几乎所有的文章都把“utf8”当成是真正的UTF-8。

“utf8”只能算是个专有的字符集,它给我们带来了新问题,却一直没有得到解决。

总结

如果你在使用MySQL或MariaDB,不要用“utf8”编码,改用“utf8mb4”。这里(https://mathiasbynens.be/notes/mysql-utf8mb4#utf8-to-utf8mb4)提供了一个指南用于将现有数据库的字符编码从“utf8”转成“utf8mb4”。

mongodb集群故障转移实践

简介

NOSQL有这些优势:

  1. 大数据量,可以通过廉价服务器存储大量的数据,轻松摆脱传统mysql单表存储量级限制。
  2. 高扩展性,Nosql去掉了关系数据库的关系型特性,很容易横向扩展,摆脱了以往老是纵向扩展的诟病。
  3. 高性能,Nosql通过简单的key-value方式获取数据,非常快速。还有NoSQL的Cache是记录级的,是一种细粒度的Cache,所以NoSQL在这个层面上来说就要性能高很多。
  4. 灵活的数据模型,NoSQL无需事先为要存储的数据建立字段,随时可以存储自定义的数据格式。而在关系数据库里,增删字段是一件非常麻烦的事情。如果是非常大数据量的表,增加字段简直就是一个噩梦。
  5. 高可用,NoSQL在不太影响性能的情况,就可以方便的实现高可用的架构。比如mongodb通过mongos、mongo分片就可以快速配置出高可用配置。
  6. 支持查询、聚合、完全索引,包含内部对象
  7. 支持复制和故障转移、自动恢复
  8. 易扩展

在nosql数据库里,大部分的查询都是键值对(key、value)的方式。MongoDB是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中最像关系数据库的。支持类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。所以这个非常方便,我们可以用sql操作MongoDB,从关系型数据库迁移过来,开发人员学习成本会大大减少。如果再对底层的sql API做一层封装,开发基本可以感觉不到mongodb和关系型数据库的区别。

  MongoDB是一个基于分布式文件存储的数据库。由C++语言编写;旨在为WEB应用提供可扩展的高性能数据存储解决方案。

安装mongodb

安装环境

  • 操作系统:Centos7.2

  • mongodb版本: v3.6.1

下载安装

wget http://fastdl.mongodb.org/linux/mongodb-linux-x86_64-amazon-3.6.1.tgz
tar zxvf mongodb-linux-x86_64-amazon-3.6.1.tgz 
mv  /root/mongodb-linux-x86_64-amazon-3.6.1 /usr/local/mongodb/

创建数据/日志目录

mkdir -p /data/mongodb/{data, logs}

mkdir /data/mongodb/data/mongod
touch /data/mongodb/logs/mongo.logs

创建配置文件

mkdir /usr/local/mongodb/config
cd  /usr/local/mongodb/config && touch mongo.conf

配置文件

1.普通配置文件示例

dbpath=/data/mongodb/data/mongod
logpath=/data/mongodb/logs/mongo.log
logappend=true
replSet=mongo-rs
bind_ip=0.0.0.0
port=27017
fork=true
journal=true

mongodb3.x版本后就是要yaml语法格式的配置文件,下面是yaml配置文件格式如下:
官方yaml配置文件选项参考:https://docs.mongodb.org/manual/reference/configuration-options/#configuration-file
注意:只能使用空格,不支持tab键

2.yaml格式配置文件示例

storage:
    dbPath: /data/mongodb/data/mongod
    journal:
        enabled: true
systemLog:
    destination: file
    path: /data/mongodb/logs/mongo.log
    logAppend: true
    logRotate: rename
net:
    bindIp: 0.0.0.0
    port: 27017
processManagement:
    pidFilePath: /var/run/pid/mongodb.pid
    fork: true
replication:
    oplogSizeMB: 20480
    replSetName: mongo-rs

配置文件参数说明

1.基本参数

--quiet     # 安静输出
--port arg  # 指定服务端口号,默认端口27017
--bind_ip arg   # 绑定服务IP,若绑定127.0.0.1,则只能本机访问,不指定默认本地所有IP
--logpath arg   # 指定MongoDB日志文件,注意是指定文件不是目录
--logappend     # 使用追加的方式写日志
--pidfilepath arg   # PID File 的完整路径,如果没有设置,则没有PID文件
--keyFile arg   # 集群的私钥的完整路径,只对于Replica Set 架构有效
--unixSocketPrefix arg  # UNIX域套接字替代目录,(默认为 /tmp)
--fork  # 以守护进程的方式运行MongoDB,创建服务器进程
--auth  # 启用验证
--cpu   # 定期显示CPU的CPU利用率和iowait
--dbpath arg    # 指定数据库路径
--diaglog arg   # diaglog选项 0=off 1=W 2=R 3=both 7=W+some reads
--directoryperdb    # 设置每个数据库将被保存在一个单独的目录
--journal   # 启用日志选项,MongoDB的数据操作将会写入到journal文件夹的文件里
--journalOptions arg    # 启用日志诊断选项
--ipv6  # 启用IPv6选项
--jsonp     # 允许JSONP形式通过HTTP访问(有安全影响)
--maxConns arg  # 最大同时连接数 默认2000
--noauth    # 不启用验证
--nohttpinterface   # 关闭http接口,默认关闭27018端口访问
--noprealloc    # 禁用数据文件预分配(往往影响性能)
--noscripting   # 禁用脚本引擎
--notablescan   # 不允许表扫描
--nounixsocket  # 禁用Unix套接字监听
--nssize arg (=16)  # 设置信数据库.ns文件大小(MB)
--objcheck  # 在收到客户数据,检查的有效性,
--profile arg   # 档案参数 0=off 1=slow, 2=all
--quota     # 限制每个数据库的文件数,设置默认为8
--quotaFiles arg    # number of files allower per db, requires --quota
--rest  # 开启简单的rest API
--repair    # 修复所有数据库run repair on all dbs
--repairpath arg    # 修复库生成的文件的目录,默认为目录名称dbpath
--slowms arg (=100)     # value of slow for profile and console log
--smallfiles    # 使用较小的默认文件
--syncdelay arg (=60)   # 数据写入磁盘的时间秒数(0=never,不推荐)
--sysinfo   # 打印一些诊断系统信息
--upgrade   # 如果需要升级数据库

2.Replicaton 参数

--fastsync  # 从一个dbpath里启用从库复制服务,该dbpath的数据库是主库的快照,可用于快速启用同步
--autoresync    # 如果从库与主库同步数据差得多,自动重新同步,
--oplogSize arg     # 设置oplog的大小(MB)

3.主/从参数

--master    # 主库模式
--slave     # 从库模式
--source arg    # 从库 端口号
--only arg  # 指定单一的数据库复制
--slavedelay arg    # 设置从库同步主库的延迟时间

4.Replica set(副本集)选项

--replSet arg   # 设置副本集名称 

Sharding(分片)选项
--configsvr     # 声明这是一个集群的config服务,默认端口27019,默认目录/data/configdb
--shardsvr  # 声明这是一个集群的分片,默认端口27018
--noMoveParanoia    # 关闭偏执为moveChunk数据保存

启动

mongod –quiet -f /usr/local/mongodb/config/mongo.conf
配置文件里设置里fork:true,所以会在后台启动,值得注意的是,用到了”–fork”参数就必须启用”–logpath”参数,如不指定配置文件启动,如下:

mongod --dbpath=/data/mongodb/data/mongod --fork --logpath=/data/mongodb/logs/mongo.logs

集群搭建

官方不建议再使用主从集群模式,推荐的集群方式是Replica Set(副本集),主从模式其实就是一个单副本的应用,没有很好的扩展性和容错性。而副本集具有多个副本保证了容错性,就算一个副本挂掉了还有很多副本存在,并且解决了上面第一个问题“主节点挂掉了,整个集群内会自动切换”。

副本集的设计结构

未分类

由图可以看到客户端连接到整个副本集,不关心具体哪一台机器是否挂掉。主服务器负责整个副本集的读写,副本集定期同步数据备份,一但主节点挂掉,副本节点就会选举一个新的主服务器,这一切对于应用服务器不需要关心。

未分类

配置步骤

准备三台机器

172.29.142.17  主
172.29.142.18  备
172.28.226.199 仲裁

按照第二步安装依次在三台机器上安装并启动

/usr/local/mongodb/bin/mongod --quiet -f /usr/local/mongodb/config/mongo.conf

初始化集群配置

三台服务启动并不能表示他们在一个集群,因此需要将集群初始化。连接任意一个节点(不要是仲裁点),执行如下:

rs.initiate({
 _id:"mongo-rs", #集群名称 
 members:[ {_id:0,host:'172.29.142.18:27017',priority:2}, #主
 {_id:1,host:'172.29.142.17:27017',priority:1}, #备
 {_id:2,host:'172.28.226.199:27017',arbiterOnly:true}]  #仲裁节点
}) 

成功上面会返回OK,然后查看集群状态,下面是在备节点上执行的

rs.status()

返回集群的名称和members信息,如:

{
    "set" : "mongo-rs",
    "date" : ISODate("2018-06-26T14:56:08.032Z"),
    "myState" : 2,
    "term" : NumberLong(2),
    "syncingTo" : "172.29.142.18:27017",
    "heartbeatIntervalMillis" : NumberLong(2000),
    "optimes" : {
        "lastCommittedOpTime" : {
            "ts" : Timestamp(1530024958, 1),
            "t" : NumberLong(2)
        },
        "appliedOpTime" : {
            "ts" : Timestamp(1530024958, 1),
            "t" : NumberLong(2)
        },
        "durableOpTime" : {
            "ts" : Timestamp(1530024958, 1),
            "t" : NumberLong(2)
        }
    },
    "members" : [ 
        {
            "_id" : 0,
            "name" : "172.29.142.18:27017",
            "health" : 1.0,
            "state" : 1,
            "stateStr" : "PRIMARY",
            "uptime" : 382251,
            "optime" : {
                "ts" : Timestamp(1530024958, 1),
                "t" : NumberLong(2)
            },
            "optimeDurable" : {
                "ts" : Timestamp(1530024958, 1),
                "t" : NumberLong(2)
            },
            "optimeDate" : ISODate("2018-06-26T14:55:58.000Z"),
            "optimeDurableDate" : ISODate("2018-06-26T14:55:58.000Z"),
            "lastHeartbeat" : ISODate("2018-06-26T14:56:07.329Z"),
            "lastHeartbeatRecv" : ISODate("2018-06-26T14:56:06.453Z"),
            "pingMs" : NumberLong(0),
            "electionTime" : Timestamp(1529642739, 1),
            "electionDate" : ISODate("2018-06-22T04:45:39.000Z"),
            "configVersion" : 1
        }, 
        {
            "_id" : 1,
            "name" : "172.29.142.17:27017",
            "health" : 1.0,
            "state" : 2,
            "stateStr" : "SECONDARY",
            "uptime" : 382552,
            "optime" : {
                "ts" : Timestamp(1530024958, 1),
                "t" : NumberLong(2)
            },
            "optimeDate" : ISODate("2018-06-26T14:55:58.000Z"),
            "syncingTo" : "172.29.142.18:27017",
            "configVersion" : 1,
            "self" : true
        }, 
        {
            "_id" : 2,
            "name" : "172.28.226.199:27017",
            "health" : 1.0,
            "state" : 7,
            "stateStr" : "ARBITER",
            "uptime" : 168617,
            "lastHeartbeat" : ISODate("2018-06-26T14:56:06.895Z"),
            "lastHeartbeatRecv" : ISODate("2018-06-26T14:56:04.092Z"),
            "pingMs" : NumberLong(35),
            "configVersion" : 1
        }
    ],
    "ok" : 1.0
}

View Code

返回参数说明

“health” : 1, #代表机器正常
“stteStr” : “PRIMARY”, #代表是主节点,可读写,其中有以下几下状态:

STARTUP:刚加入到复制集中,配置还未加载
STARTUP2:配置已加载完,初始化状态
RECOVERING:正在恢复,不适用读
ARBITER: 仲裁者
DOWN:节点不可到达
UNKNOWN:未获取其他节点状态而不知是什么状态,一般发生在只有两个成员的架构,脑裂
REMOVED:移除复制集
ROLLBACK:数据回滚,在回滚结束时,转移到RECOVERING或SECONDARY状态
FATAL:出错。查看日志grep “replSet FATAL”找出错原因,重新做同步
PRIMARY:主节点
SECONDARY:备份节点

测试副本集数据复制

注意:mongodb默认是从主节点读写数据的,副本节点上不允许读,需要设置副本节点可以读:

repset:SECONDARY> db.getMongo().setSlaveOk();

这个很好测试,直接在主节点插入一条数据,在备节点查询即可

或者可以使用客户端以集群模式连接mongo集群:

未分类

点Test 测试连接:

未分类

三个节点的数据是同步的。

测试副本集故障转移功能

1.查看集群当前状态,如上返回

当前172.29.142.18是Primary, 172.29.142.17是Secondary

未分类

2.停掉主节点172.29.142.18,查看另两台的选票结果

未分类

此时17变成了主节点,原先的仲裁节点不变,重新启动第一次的Primary,则主节点又发生变化,不再截图,整个过程业务是不中断的。只要有一台可用即可。

Nodejs连接mongo集群示例

这里强烈不推荐连接单台mongo服务,因为如果一个mongo节点挂掉,业务就挂了,连接集群的话有一台可用就行。

下面举了个nodejs连接mongo集群的示例:

const mongoose = require('mongoose');
let url = "mongodb://172.29.142.17:27017/testdb,mongodb://172.29.142.18:27017/testdb,mongodb://172.28.226.199:27017/testdb";
let options = {
  "replset": {
    "ha": true,
    "haInterval": 1000,
    "replicaSet": "mongo-rs",
    "connectWithNoPrimary": true,
    "auto_reconnect": true,
    "socketOptions": {
      "keepAlive": 120,
      connectTimeoutMS: 30000
    }
  }
}


mongoose.connect(url, options).connection
  .on('error', function (error) {
    console.log('mongo 连接错误', error)
  }).on('disconnected', mongoConnect).once('open', function () {
    console.log('mongo 连接成功');
  })

Amazon Linux安装Mariadb

|默认输入yum -y mariadb mariadb-server

提示No package mariadb , mariadb-server

第一步,创建repo

vim /etc/yum.repos.d/MariaDB.repo

内容:

# http://downloads.mariadb.org/mariadb/repositories/
[mariadb]
name = MariaDB
baseurl = http://yum.mariadb.org/10.2/centos6-amd64
gpgkey=https://yum.mariadb.org/RPM-GPG-KEY-MariaDB
gpgcheck=1

保存,同时更新yum

yum clean all
yum update

最后再次运行:

yum install -y MariaDB-server MariaDB-client

搞定,安装完成

Kubernetes调度之亲和性和反亲和性

背景

Kubernetes中的调度策略可以大致分为两种,一种是全局的调度策略,要在启动调度器时配置,包括kubernetes调度器自带的各种predicates和priorities算法,具体可以参看文章《Kubernetes调度详解》;另一种是运行时调度策略,包括nodeAffinity(主机亲和性),podAffinity(POD亲和性)以及podAntiAffinity(POD反亲和性)。

nodeAffinity 主要解决POD要部署在哪些主机,以及POD不能部署在哪些主机上的问题,处理的是POD和主机之间的关系。

podAffinity 主要解决POD可以和哪些POD部署在同一个拓扑域中的问题(拓扑域用主机标签实现,可以是单个主机,也可以是多个主机组成的cluster、zone等。),podAntiAffinity主要解决POD不能和哪些POD部署在同一个拓扑域中的问题。它们处理的是Kubernetes集群内部POD和POD之间的关系。

三种亲和性和反亲和性策略的比较如下表所示:

未分类

本文主要介绍如何使用亲和性和反亲和性做资源调度。

使用场景

nodeAffinity使用场景 :

  • 将S1服务的所有Pod部署到指定的符合标签规则的主机上。
  • 将S1服务的所有Pod部署到除部分主机外的其他主机上。

podAffinity使用场景 :

  • 将某一特定服务的pod部署在同一拓扑域中,不用指定具体的拓扑域。
  • 如果S1服务使用S2服务,为了减少它们之间的网络延迟(或其它原因),把S1服务的POD和S2服务的pod部署在同一拓扑域中。

podAntiAffinity使用场 景:

  • 将一个服务的POD分散在不同的主机或者拓扑域中,提高服务本身的稳定性。
  • 给POD对于一个节点的独占访问权限来保证资源隔离,保证不会有其它pod来分享节点资源。
  • 把可能会相互影响的服务的POD分散在不同的主机上。

对于亲和性和反亲和性,每种都有三种规则可以设置:

RequiredDuringSchedulingRequiredDuringExecution :在调度期间要求满足亲和性或者反亲和性规则,如果不能满足规则,则POD不能被调度到对应的主机上。在之后的运行过程中,如果因为某些原因(比如修改label)导致规则不能满足,系统会尝试把POD从主机上删除(现在版本还不支持)。

RequiredDuringSchedulingIgnoredDuringExecution :在调度期间要求满足亲和性或者反亲和性规则,如果不能满足规则,则POD不能被调度到对应的主机上。在之后的运行过程中,系统不会再检查这些规则是否满足。

PreferredDuringSchedulingIgnoredDuringExecution :在调度期间尽量满足亲和性或者反亲和性规则,如果不能满足规则,POD也有可能被调度到对应的主机上。在之后的运行过程中,系统不会再检查这些规则是否满足。

使用示例

使用POD亲和性调度时要先开启Kubernetes调度器的MatchInterPodAffinity筛选功能,具体的操作方式是修改调度器的配置文件,在predicates中增加如下内容:

{"name": "MatchInterPodAffinity"}

测试环境的主机信息如下:

未分类

其中每个主机上都有 beta.kubernetes.io/arch、beta.kubernetes.io/os、kubernetes.io/hostname这几个标签,在测试过程中把这些标签当做拓扑域使用。

nodeAffinity 使用示例:

使用nodeAffinity把POD部署到主机mesos-slave1和mesos-slave2上,yaml定义如下:

{
  "nodeAffinity": {
    "requiredDuringSchedulingIgnoredDuringExecution": {
      "nodeSelectorTerms": [
        {
          "matchExpressions": [
            {
              "key": "kubernetes.io/hostname",
              "operator": "In",
              "values": [
                "mesos-slave1",
                "mesos-slave2"
              ]
            }
          ]
        }
      ]
    }
  }
}

创建一个有6个POD的RC,结果如下:

未分类

从结果可以看出POD被部署到了mesos-slave1和mesos-slave2上,mesos-slave3上没有部署POD。

podAffinity使用示例 :

使用kubernetes.io/hostname作为拓扑域,把pod创建在同一主机上。其中matchExpressions中填写内容对应到RC中POD自身的标签。可以通过修改需要匹配的标签内容来控制把一个服务中的POD和其它服务的POD部署在同一主机上。

yaml中的定义如下:

{
  "podAffinity": {
    "requiredDuringSchedulingIgnoredDuringExecution": [
      {
        "labelSelector": {
          "matchExpressions": [
            {
              "key": "name",
              "operator": "In",
              "values": [
                "node-rc"
              ]
            }
          ]
        },
        "topologyKey": "kubernetes.io/hostname"
      }
    ]
  }
}

创建一个有3个POD的RC,结果如下:

未分类

所有创建的POD集中在同一个主机上,具体的主机是哪个不需要指定。

podAntiAffinity 使用示例:

使用kubernetes.io/hostname作为拓扑域,把pod创建在不同主机上,每个主机上最多只有一个同类型的POD(同类型用标签区分)。其中matchExpressions中填写内容对应到RC中POD自身的标签。可以通过修改需要匹配的标签内容来控制把一个服务中的POD和其它服务的POD部署在不同主机上。

yaml中的定义如下:

{
  "podAntiAffinity": {
    "requiredDuringSchedulingIgnoredDuringExecution": [
      {
        "labelSelector": {
          "matchExpressions": [
            {
              "key": "name",
              "operator": "In",
              "values": [
                "node-rc"
              ]
            }
          ]
        },
        "topologyKey": "kubernetes.io/hostname"
      }
    ]
  }
}

创建一个有4个POD的RC,结果如下:

未分类

三个主机上都有一个POD运行,因为每个主机上最多只能运行一个这种类型的POD,所以有一个POD一直处于Pending状态,不能调度到任何节点。

上边的例子中可以通过修改topologyKey来限制拓扑域的范围,实现把相关服务部署在不同的容灾域等其它功能。

总结

Kubernetes提供了丰富的调度策略,包括静态的全局调度策略,以及动态的运行时调度策略,用户可以根据需要自由组合使用这些策略来实现自己的需求。在调度过程中,使用nodeAffnity决定资源可以部署在哪些主机上,使用podAffinity和podAntiAffinity决定哪些资源需要部署在同一主机(拓扑域)或者不能部署在同一主机。

kubernetes集群使用Ceph

https://blog.csdn.net/aixiaoyang168/article/details/78999851

经实验,在一个node上多个Pod是可以以ReadWrite模式挂载同一个CephRBD,但是跨node则不行,会提示image xxx is locked by other nodes。而我们的应用场景是需要多个node挂载一个ceph的,在我们的应用场景需要使用CephFS。

使用cephfs的场景:创建一个fs,挂载的时候指定path。

kubernetes使用CephFS的两种方式:

1.直接通过pod挂载

apiVersion: v1
kind: Pod
metadata:
name: cephfs2
spec:
containers:
- name: cephfs-rw
image: busybox
command: ["sleep", "60000"]
volumeMounts:
- mountPath: "/mnt/cephfs"
name: cephfs
volumes:
- name: cephfs
cephfs:
monitors:
- '<your_etcd_ip>:6789'
user: admin
secretRef:
name: ceph-secret
readOnly: false

2.通过创建pv、pvc挂载

在ceph集群上找到key:

[cephd@<your_ceph_machine> ~]$ ceph auth get-key client.admin | base64
QVFBNEhnNWJpQmN1RWhBQUhWSmJKZTVtOG9jWUdkNmlYMnA5dmc9PQ==

创建secret:

apiVersion: v1
kind: Secret
metadata:
name: ceph-secret
data:
key: QVFBNEhnNWJpQmN1RWhBQUhWSmJKZTVtOG9jWUdkNmlYMnA5dmc9PQ==

PV:

apiVersion: v1
kind: PersistentVolume
metadata:
name: cephfs-pv
spec:
capacity:
storage: 1Gi
accessModes:
– ReadWriteMany
cephfs:
monitors:
– <your_etcd_ip>:6789

path: /sns
user: admin
secretRef:
name: ceph-secret
readOnly: false
persistentVolumeReclaimPolicy: Recycle

PVC:

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: cephfs-pv-claim
spec:
accessModes:
– ReadWriteMany
resources:
requests:
storage: 1Gi

创建POD:

apiVersion: v1
kind: Pod
metadata:
labels:
test: cephfs-pvc-pod
name: cephfs-pv-pod1
spec:
containers:
– name: cephfs-pv-busybox1
image: busybox
command: [“sleep”, “60000”]
volumeMounts:
– mountPath: “/mnt/cephfs”
name: cephfs-vol1
readOnly: false
volumes:
– name: cephfs-vol1
persistentVolumeClaim:
claimName: cephfs-pv-claim

遇到的问题:

1.映射到内核的时候报错RBD image feature set mismatch

http://blog.51cto.com/hipzz/1888048

–image-format format-id

format-id取值为1或2,默认为 2。

format 1 – 新建 rbd 映像时使用最初的格式。此格式兼容所有版本的 librbd 和内核模块,但是不支持较新的功能,像克隆。

format 2 – 使用第二版 rbd 格式, librbd 和 3.11 版以上内核模块才支持(除非是分拆的模块)。此格式增加了克隆支持,使得扩展更容易,还允许以后增加新功能。

解决方案1:

更改为格式1,重新映射。

注意:需要重新建立镜像。

[root@ceph1 ~]# rbd create block1 –image-format 1 –size 1024
rbd: image format 1 is deprecated
[root@ceph1 ~]# rbd ls
block1
block
[root@ceph1 ~]# rbd map block1
/dev/rbd0
[root@ceph1 ~]#

d.如上所示,映射正确。

解决方案2:

根据官网介绍,新建rbd默认格式2的rbd 块支持如下特性,并且默认全部开启:

–image-feature:

layering: 支持分层

striping: 支持条带化 v2

exclusive-lock: 支持独占锁

object-map: 支持对象映射(依赖 exclusive-lock )

fast-diff: 快速计算差异(依赖 object-map )

deep-flatten: 支持快照扁平化操作

journaling: 支持记录 IO 操作(依赖独占锁)

接下来尝试少开启一些特性:

[root@ceph1 ~]# rbd create block2 –image-feature layering –size 1024
[root@ceph1 ~]# rbd map block2
/dev/rbd1

2.创建pod挂载的时候遇到rbd: map failed executable file not found in $PATH

k8s集群内的节点上需要安装ceph-client:

yum install ceph–common

3.umount的时候出现target is busy

umount -l xxx

https://www.cnblogs.com/dkblog/archive/2012/07/18/2597192.html

https://blog.csdn.net/u012207077/article/details/21159339

4.如果k8s的node跟ceph集群的node不一样,则需要在k8s的node上部署ceph-common

yum install ceph-common

5.创建pod的时候提示,mount过去的时候提示libceph: bad option

k8s secret 认证 key 需要使用 base64 编码,有可能是secret文件里的key没有base64编码:

在ceph节点上ceph auth get-key client.admin |base64

填到secret文件里面。

6.如果mount fail,则去机器上查看kubelet的日志

7.多用户隔离

https://www.jianshu.com/p/96a34485f0fc

需要用pool,给user指定目录和权限,之后在pv中使用。

8.mount子目录

https://www.spinics.net/lists/ceph-devel/msg34698.html

mount -t ceph >> 172.24.0.4:6789:/volumes/kubernetes/test1 /tmp/mnt -o >> name=bar,secret=AQA+ln9Yfm6DKhAA10k7QkdkfIAKqmM6xeCsxA==

9.写入到共享存储的时候提示File Exists

目录权限问题,需要与Dockerfile中指定的USER的权限一样

HTTPS服务的Kubernetes ingress配置实践

在公有云被广泛接纳的今天,数据传输安全问题日益凸显,因为在公有云提供商的经典网络(二层互通)中,即便是内部网络通信也要考虑网络嗅探等hack手段,这也是公有云主推所谓“专用网络(二层隔离)”的原因之一。从应用的角度,我们应该尽量通过技术手段保证数据通信的安全性。而目前最常用的方式就是基于SSL/TLS的安全通信方式了,在七层,对应的就是https了。

这样,下面的仅在负载均衡/反向代理入口做加密通信的传统模型越来越无法满足数据安全性的需要了(nginx与backend service之间是基于明文的http通信):

传统安全通信模型:

client --- (via https) ---> nginx ---- (via http) ----> upstream backend services

我们需要下面的模型:

更为安全的通信模型:

client --- (via https) ---> nginx ---- (via https) ----> upstream backend services

在Kubernetes集群中,这种情况稍好些,首先,业务负载运行在集群的“虚拟网络”中,其次,一些K8s的网络插件实现是支持跨节点网络加密的(有一定的网络性能损耗),比如weave。但永远没有绝对的安全,作为业务应用的设计和实现人员,我们要尽可能的保证数据的通信安全,因此在面向七层的应用中,要尽可能的使用基于HTTPS的通信模型。本篇就来实践一下如何为Kubernetes集群内的HTTPS服务进行ingress的配置。

一. 例子概述与环境准备

在《实践kubernetes ingress controller的四个例子》一文中,我讲解了四种基本的kubernetes ingress配置方式。在这些例子中,有些例子的ingress controller(nginx)与backend service之间使用的是https,但client到ingress controller之间的通信却一直是基于http的。在本文中,我们的目标就是上面提到的那个更为安全的通信模型,即client与ingress controller(nginx)、nginx与backend service之间均使用的是https通信。这里在《实践kubernetes ingress controller的四个例子》一文例子的基础上,我们创建一个新的nginx ingress controller: nginx-ingress-controller-ic3,并将后端的svc7~svc9三个不同类型的服务暴露给client,如下图所示:

未分类

  • svc7: 是对传统通信模型的“复现”,即client与ingress controller(nginx)间采用https加密通信,但ingress controller(nginx)与svc7间则是明文的http通信;
  • svc8: 是ssl-termination的安全配置模型,即client与svc8的https通信分为“两段”,client与nginx建立https连接后,nginx将client提交的加密请求解密后,再向svc8发起https请求,并重新加密请求数据。这种client端ssl的过程在反向代理或负载均衡器终结的https通信方式被称为“ssl-termination”。
  • svc9: 是ssl-passthrough的安全配置模型,即nginx不会对client的https request进行解密,而是直接转发给backend的svc9服务,client端的ssl过程不会终结于nginx,而是在svc9对应的pod中终结。这种https通信方式被称为”ssl-passthrough”。这种配置模型尤其适合backend service对client端进行client certificate验证的情况,同时也降低了nginx加解密的性能负担。

本文基于下面环境进行实验:kubernetes 1.10.3、weave networks 2.3.0、nginx-ingress-controller:0.15.0。关于本文涉及的例子的源码、chart包以及ingress controllers的yaml源文件可以在这里下载到。

二. 建立新的ingress-nginx-controller:nginx-ingress-controller-ic3

为了更好地进行例子说明,我们建立一个新的ingress-nginx-controller:nginx-ingress-controller-ic3,svc7~svc9都通过该ingress controller进行服务入口的暴露管理。要创建nginx-ingress-controller-ic3,我们首先需要在ic-common.yaml中为Role: nginx-ingress-role添加一个resourceName: “ingress-controller-leader-ic3″,并apply生效:

// ic-common.yaml
... ...
    resourceNames:
      # Defaults to "<election-id>-<ingress-class>"
      # Here: "<ingress-controller-leader>-<nginx>"
      # This has to be adapted if you change either parameter
      # when launching the nginx-ingress-controller.
      - "ingress-controller-leader-ic1"
      - "ingress-controller-leader-ic2"
      - "ingress-controller-leader-ic3"
... ...

# kubectl apply -f ic-common.yaml

我们为nginx-ingress-controller-ic3创建nodeport service,新nodeport为:30092:

// ic3-service-nodeport.yaml
apiVersion: v1
kind: Service
metadata:
  name: ingress-nginx-ic3
  namespace: ingress-nginx-demo
spec:
  type: NodePort
  ports:
  - name: https
    port: 443
    targetPort: 443
    nodePort: 30092
    protocol: TCP
  selector:
    app: ingress-nginx-ic3

注意:ingress-nginx-ic3 service的nodeport映射到ic3 ingress controller的443端口,也就是支持安全通信的端口,而不是明文的80端口。

最后创建nginx-ingress-controller-ic3 pod,可以复制一份ic2-mandatory.yaml,然后将内容中的ic2全部修改为ic3即可:

# kubectl apply -f ic3-mandatory.yaml

如无意外,nginx-ingress-controller-ic3应该已经正常地运行在你的k8s cluster中了。

三. svc7: 使用ssl termination,但nginx与backend服务之间采用明文传输(http)

加密Web流量有两个主要配置方案:SSL termination和SSL passthrough。

使用SSL termination时,客户端的SSL请求在负载均衡器/反向代理中解密,解密操作将增加负载均衡器的工作负担,较为耗费CPU,但简化了SSL证书的管理。至于负载均衡器和后端之间的流量是否加密,需要nginx另行配置。

SSL Passthrough,意味着client端将直接将SSL连接发送到后端(backend)。与SSL termination不同,请求始终保持加密,并且解密负载分布在后端服务器上。但是,这种情况的SSL证书管理略复杂,证书必须在每台服务器上自行管理。另外,在这种方式下可能无法添加或修改HTTP header,可能会丢失X-forwarded-* header中包含的客户端的IP地址,端口和其他信息。

我们先来看一种并不那么“安全”的“传统模型”:在nginx上暴露https,但nginx到backend service(svc7)采用http。

我们先来创建相关的密钥和公钥证书,并以一个Secret:ingress-controller-demo-tls-secret存储密钥和证书数据:

// ingress-controller-demo/manifests下面

# openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout ic3.key -out ic3.crt -subj "/CN=*.tonybai.com/O=tonybai.com"
# kubectl create secret tls ingress-controller-demo-tls-secret --key  ic3.key --cert ic3.crt

svc7几乎是和svc1一样的程序(输出的字符串标识不同),但svc7的ingress与svc1大不相同,因为我们需要通过https访问svc7的ingress:

// svc7的values.yaml
... ...
replicaCount: 1

image:
  repository: bigwhite/ingress-controller-demo-svc7
  tag: v0.1
  pullPolicy: Always

service:
  type: ClusterIP
  port: 443

ingress:
  enabled: true
  annotations:
    kubernetes.io/ingress.class: ic3
  path: /
  hosts:
    - svc7.tonybai.com
  tls:
    - secretName: ingress-controller-demo-tls-secret
      hosts:
        - svc7.tonybai.com
... ...

与svc1的values.yaml不同的是,我们使用的ingress controller是ic3,我们开启了tls,secret用的就是我们上面创建的那个secret:ingress-controller-demo-tls-secret。创建ic3-svc7后,我们看到ingress controller内部的nginx.conf中有关svc7的配置输出如下:

# kubectl exec nginx-ingress-controller-ic3-67f7cf7845-2tnc9 -n ingress-nginx-demo -- cat /etc/nginx/nginx.conf

        # map port 442 to 443 for header X-Forwarded-Port
        map $pass_server_port $pass_port {
                442              443;
                default          $pass_server_port;
        }

        upstream default-ic3-svc7-http {
                least_conn;

                keepalive 32;

                server 192.168.28.13:8080 max_fails=0 fail_timeout=0;

        }

## start server svc7.tonybai.com
        server {
                server_name svc7.tonybai.com ;

                listen 80;

                listen [::]:80;

                set $proxy_upstream_name "-";

                listen 442 proxy_protocol   ssl http2;

                listen [::]:442 proxy_protocol  ssl http2;

                # PEM sha: 248951b75535e0824c1a7f74dc382be3447057b7
                ssl_certificate                         /ingress-controller/ssl/default-ingress-controller-demo-tls-secret.pem;
                ssl_certificate_key                     /ingress-controller/ssl/default-ingress-controller-demo-tls-secret.pem;

                ssl_trusted_certificate                 /ingress-controller/ssl/default-ingress-controller-demo-tls-secret-full-chain.pem;
                ssl_stapling                            on;
                ssl_stapling_verify                     on;

                location / {
                        ... ...
                        proxy_pass http://default-ic3-svc7-http;

                        proxy_redirect                          off;

                }
           ... ...
        }
        ## end server svc7.tonybai.com

可以看到30092(nodeport) 映射的ingress controller的443端口在svc7.tonybai.com这个server域名下已经有了ssl标识,并且ssl_certificate和ssl_certificate_key对应的值就是我们之前创建的ingress-controller-demo-tls-secret。

我们通过curl访问以下svc7服务:

# curl -k https://svc7.tonybai.com:30092
Hello, I am svc7 for ingress-controller demo!

此时,如果再用http方式去访问svc7,你会得到下面错误结果:

# curl http://svc7.tonybai.com:30092
<html>
<head><title>400 The plain HTTP request was sent to HTTPS port</title></head>
<body bgcolor="white">
<center><h1>400 Bad Request</h1></center>
<center>The plain HTTP request was sent to HTTPS port</center>
<hr><center>nginx/1.13.12</center>
</body>
</html>

四. svc8: 使用ssl termination,但nginx与backend服务之间采用加密传输(https)

前面说过,SSL termination配置场景中,负载均衡器和后端之间的流量是否加密,需要nginx另行配置。svc7采用了未加密的方式,nginx -> backend service存在安全风险,我们要将其改造为也通过https进行数据加密传输,于是有了svc8这个例子。

svc8对应的程序本身其实是上一篇文章《实践kubernetes ingress controller的四个例子》中的svc2的clone(唯一修改就是输出的log中的标识)。

在svc8对应的chart中,我们将values.yaml改为:

// ingress-controller-demo/charts/svc8/values.yaml

replicaCount: 1

image:
  repository: bigwhite/ingress-controller-demo-svc8
  tag: v0.1
  pullPolicy: Always

service:
  type: ClusterIP
  port: 443

ingress:
  enabled: true
  annotations:
    # kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/secure-backends: "true"
    kubernetes.io/ingress.class: ic3
  path: /
  hosts:
    - svc8.tonybai.com
  tls:
    - secretName: ingress-controller-demo-tls-secret
      hosts:
        - svc8.tonybai.com

... ...

与svc7不同点在于values.yaml中的新annotation: nginx.ingress.kubernetes.io/secure-backends: “true”。这个annotation让nginx以https的方式去访问backend service: svc8。安装svc8 chart后,ingress nginx controller为svc8生成的配置如下:

## start server svc8.tonybai.com
        server {
                server_name svc8.tonybai.com ;

                listen 80;

                listen [::]:80;

                set $proxy_upstream_name "-";

                listen 442 proxy_protocol   ssl http2;

                listen [::]:442 proxy_protocol  ssl http2;

                # PEM sha: 248951b75535e0824c1a7f74dc382be3447057b7
                ssl_certificate                         /ingress-controller/ssl/default-ingress-controller-demo-tls-secret.pem;
                ssl_certificate_key                     /ingress-controller/ssl/default-ingress-controller-demo-tls-secret.pem;

                ssl_trusted_certificate                 /ingress-controller/ssl/default-ingress-controller-demo-tls-secret-full-chain.pem;
                ssl_stapling                            on;
                ssl_stapling_verify                     on;

                location / {
                     ... ...
                        proxy_pass https://default-ic3-svc8-https;

                        proxy_redirect                          off;

                }

        }
        ## end server svc8.tonybai.com

        upstream default-ic3-svc8-https {
                least_conn;

                keepalive 32;

                server 192.168.28.14:8080 max_fails=0 fail_timeout=0;

        }

使用curl访问svc8服务(-k: 忽略对server端证书的校验):

# curl -k https://svc8.tonybai.com:30092
Hello, I am svc8 for ingress-controller demo!

五. svc9: 使用ssl passthrough, termination at pod

某些服务需要通过对client端的证书进行校验的方式,进行身份验证和授权,svc9就是这样一个对client certification进行校验的双向https校验的service。针对这种情况,ssl termination的配置方法无法满足需求,我们需要使用ssl passthrough的方案。

在ingress nginx controller开启ssl passthrough方案需要在ingress controller和ingress中都做一些改动。

首先我们需要为nginx-ingress-controller-ic3添加一个新的命令行参数:–enable-ssl-passthrough,并重新apply生效:

// ic3-mandatory.yaml
... ...
spec:
      serviceAccountName: nginx-ingress-serviceaccount
      containers:
        - name: nginx-ingress-controller-ic3
          image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.15.0
          args:
            - /nginx-ingress-controller
            - --default-backend-service=$(POD_NAMESPACE)/default-http-backend
            - --configmap=$(POD_NAMESPACE)/nginx-configuration-ic3
            - --tcp-services-configmap=$(POD_NAMESPACE)/tcp-services-ic3
            - --udp-services-configmap=$(POD_NAMESPACE)/udp-services-ic3
            - --publish-service=$(POD_NAMESPACE)/ingress-nginx-ic3
            - --annotations-prefix=nginx.ingress.kubernetes.io
            - --enable-ssl-passthrough
            - --ingress-class=ic3
... ...

然后在svc9的chart中,为ingress添加新的annotation
nginx.ingress.kubernetes.io/ssl-passthrough: “true”

// ingress-controller-demo/charts/svc9/values.yaml

replicaCount: 1

image:
  repository: bigwhite/ingress-controller-demo-svc9
  tag: v0.1
  pullPolicy: Always

service:
  type: ClusterIP
  port: 443

ingress:
  enabled: true
  annotations:
    kubernetes.io/ingress.class: ic3
    nginx.ingress.kubernetes.io/ssl-passthrough: "true"

  path: /
  hosts:
    - svc9.tonybai.com
  tls:
    - secretName: ingress-controller-demo-tls-secret
      hosts:
        - svc9.tonybai.com
... ...

isntall svc9 chart之后,我们用curl来访问以下svc9:

# curl -k  https://svc9.tonybai.com:30092
curl: (35) gnutls_handshake() failed: Certificate is bad

由于svc9程序对client端的certificate进行验证,没有提供client certificate的curl请求被拒绝了!svc9 pod的日志也证实了这一点:

2018/06/25 05:36:29 http: TLS handshake error from 192.168.31.10:38634: tls: client didn't provide a certificate

我们进入到ingress-controller-demo/src/svc9/client路径下,执行:

# curl -k --key ./client.key --cert ./client.crt https://svc9.tonybai.com:30092
Hello, I am svc9 for ingress-controller demo!

带上client.crt后,svc9通过了验证,返回了正确的应答。

client路径下是一个svc9专用的客户端,我们也可以执行该程序去访问svc9:

# go run client.go
Hello, I am svc9 for ingress-controller demo!

我们再看看采用ssl-passthrough方式下ingress-nginx controller的访问日志,当curl请求发出时,ingress-nginx controller并未有日志输出,因为没有在nginx处ssl termnination,从此也可以证实:nginx将client的ssl过程转发到pod中去了,即passthrough了。

51短信平台:企业级短信平台定制开发专家 https://51smspush.com/
smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。