MongoDB 内存使用

Filed in 技术 Leave a comment

都说 MongoDB 是个内存大户,但是怎么知道它到底用了多少内存呢?

先 ps 一下看看。

$ ps aux|grep mongod
mongo    26994  9.0 20.0 797264324 13243052 ?  Sl   May16 117:03 /path/to/mongodb/bin/mongod

总共 760G 多的虚拟内存,但是物理内存就只有 12.6G 。这个机器可是有 64G 内存的哦,这看起来 MongoDB 完全没用多少内存嘛。

再看看 free 的结果。

$ free -m
             total       used       free     shared    buffers     cached
Mem:         64544      64279        265          0        134      60413
-/+ buffers/cache:       3731      60813
Swap:        31999          0      31999

内存倒是占得差不多了,基本都是 cached ,也就是文件系统缓存。MongoDB 是通过 mmap 方式让操作系统来处理持久化和缓存的。每个数据文件都直接映射到某个虚拟内存地址。访问的时候如果这一页不在内存中,系统就会尝试把这一页加载进来。这些内存都是算进 cache 里的。在 mongodb 的官方文档里有这样一个说法,top 或 ps 里的 RSIZE 段显示的是机器的全部内存大小,因为 mongodb 会尽可能占用全部内存。但是事实上,这些缓存并没有算在里面。因此在 top 或 ps 中是看不出 MongoDB 的实际内存使用情况的。而 free 虽然可以看到系统的内存使用情况,但是没法确定这些内存里究竟有多少真的是 MongoDB 使用的。

还好有人做了 vmtouch 这个工具。可以检查文件在缓存中的情况,另外也可以把文件直接加载进缓存或者踢出去。只需要对 MongoDB 的所有数据文件检查一下缓存加载情况,就可以知道 MongoDB 到底缓存了多少数据了。

$ vmtouch -m4G /path/to/mongodb/data/
           Files: 256
     Directories: 3
  Resident Pages: 15465901/100219772  58G/382G  15.4%
         Elapsed: 4.072 seconds

这里 -m4G 是 vmtouch 检查的文件大小限制。MongoDB 的数据文件比较大,通常会超过默认的 500M。这样看来,缓存用了 58G,这还差不多。Resident Pages 左侧的数字是页的数量,页的数量乘以文件系统页大小才是内存使用量。页的大小可以通过

getconf PAGESIZE 

查看,通常是 4096,也就是 4KB。

MongoDB 在 NUMA 的机器上运行,并且内存被固定到一个 node 的时候,会有一个警告

WARNING: You are running on a NUMA machine. 
We suggest launching mongod like this to avoid performance problems: 
numactl –interleave=all mongod [other options]

也许是认为,这种情况下只能用上一个节点的内存。但 MongoDB 的缓存是由操作系统管理的。NUMA 似乎对此并没有影响。而内存不太小的时候 MongoDB 本身很难用掉一个节点的内存。这种情况下,是否开启 numactl –interleave=all 作用已经不大了。能做的也许只能是加内存,sharding,或者换 ssd 了。

API 双方认证探讨

Filed in 技术 2 Comments

开放 api 已是大势所趋。而 api 这种东西有个特点就是覆水难收。一旦公开出去了,被大量用户使用,一旦修改,就会让广大用户都掉坑里。所以,api 在设计之初就要尽量考虑周全,并预留扩展可能。

目前绝大多数 api 都是通过 http 协议访问。api 一般有两类,一类只涉及到提供方和使用者,另一类还涉及到最终用户。因此,前一类在认证上也只涉及两方,而后一类还涉及到用户授权,也就是通常使用的 OAuth。这里先说说第一类。比如 amazon aws 的 api、google 的地图 api 等的认证就属于这类。

双方认证的 api 通常的认证方式是使用一组 id 和密钥,用 id 来标记应用,用密钥来对请求做签名。id 和密钥一般是应用向服务方申请的。这样,服务费也可以控制 api 的使用。如果想停止某个用户访问 api,只需要取消这个用户的认证信息。即可。

举个例子,google 地图的 api,就是简单使用密钥对 url 做 HMAC_SHA1 签名。如:

$signature = hash_hmac("sha1",$url_to_sign, $key,  true);

然后把签名编码后,追加在 url 后面访问。服务方根据请求中的 id 找到对应的密钥,同样做一遍签名,如果相等则认证通过。对于一些更复杂一些的 api ,请求还可能通过 HTTP header 、post body 来传递。此时,签名就需要把所有关键信息都一起签名。为了避免参数顺序的敏感性,即仅仅改变参数顺序就会影响签名结果,通常还会要求先对参数进行排序。amazon aws 的新版认证协议还对于一些关键参数改为了每次加入一个就签名一次。这恐怕也是因为 sha1 也已经没那么可靠了吧。

这是最简单的认证方式。使用这种方式,由于密钥并没有在网络上传递,hmac sha1 算法也是不可逆的,第三方无法冒充别的用户对自己的 url 进行签名。但是这种简单的方式有个缺点。同样的 url 在签名后是可以反复使用的。因此,如果恰好第三方需要的服务或数据和你的相同,那也可以直接复制你的 url 去用,花你的 money 、读你的数据。这也叫重放攻击。

为了避免或者减少重放攻击的危害,一些 api 把时间也作为了参数之一。签名的时候也把时间一起签进去。在服务端不仅要比较签名,还要请求的时间和服务端时间的差是否在许可范围,比如 15 分钟内。amazon aws 的 api 就是如此。这样,就算截获了一个请求,也顶多在 15 分钟内有效。不过由此带来的问题是,如果时钟偏差大了,就无法正常调用服务了。因此,客户端和服务端都必须严格对时。如在 linux 上开启 ntpd 。另外,对于 15 分钟内的重复攻击,仍然没有很好的办法。这时候,能做的也许就是在业务上减少由此可能带来的损失了。比如对 aws 来说,重复申请资源会因为重名而无法通过,而重复修改或删除资源请求通常也是没意义的。而另一些资源信息,仅在 15 分钟内可以重放,也无法获得新的有价值的数据。

如果真的在意这一点又该如何呢?以上方法都是在保持协议无状态前提下的。如果放弃这个前提,变成有状态协议,就有了别的方法。比如,可以引入一次性令牌来解决。首次访问前,先去请求一个一次性令牌,之后请求服务的签名的时候,把这个令牌也一起签名,并一起传递。服务端会检查该令牌是否仍然有效。请求后,会使原来的令牌失效,同时颁发一个新的一次性令牌。这看着是不是有点眼熟?有点像是 session 的实现方式吧。session 就是在客户端和服务端传递一个令牌来标示身份,在无状态协议中模拟状态的。第三方就算劫持到令牌,因为没有密钥,也没法加以利用。而拦截到的请求在使用一次后,也已经失效。这样就有效避免了重放攻击。但是,这样也是有代价的,如此一来,服务端就必须来维护这些会话。可能会占用大量资源,并有可能由此受到拒绝服务攻击。因此,多数服务都没有采用这种方式来认证。

以上的认证方式只适合用在服务端-服务端。而不适合用在客户度,如浏览器、移动端、桌面客户端等。在这些地方使用双方认证 API ,就不可避免的要把应用的密码携带分发。这是很危险的。一旦有人从中破解获取了你的密钥,一切就全完蛋了。这时候,要通知所有终端更换密钥也是件麻烦事。通常,都需要使用应用自己的服务端作为代理来访问。至少是通过应用自己的服务端来计算签名。但是这对于一些服务,比如云存储,代价就有点大了,这样一来,应用就还是得在自己的服务器上走大量的流量。要解决这样的问题,一些 API 提供方给了另外的一种认知方式。

应用先给自己的每个用户生成一个 uid,然后用密钥对其签名得到用户密钥,把应用 id,uid,签名保存在客户端分发。从客户端发起请求时,带上应用 id,uid,使用用户密钥签名。服务端则用同样的方式验证,并可以通过 uid 来做数据的访问控制。这样一旦客户端被破解,也只会影响一个终端用户,而不是像之前一样,所有用户的信息都收到威胁。

, ,

动态修改运行中进程的 rlimit

Filed in 技术 Leave a comment

有时候发现线上运行的程序没有修改 ulimit -n ,导致文件描述符不够用,但是又不能随便重启服务,或者碰到运行中的程序可能不定时会 segment fault ,但是由于没 ulimit -c ,无法得到 core 文件调试。但是 linux 的 ulimit 命令只对当前会话有效,已经启动的进程是没法用这个命令修改限制的。这时候如果能动态修改进程的 rlimit 就会方便很多。linux 内核版本 2.6.36 以后,多了一个 prlimit 调用,可以实现动态修改某个进程的 rlimit。

例如,这个 C 写的小程序就可以放开指定进程的 core dump 限制。

#include <stdio.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <sys/types.h>

int main(int argc, char** argv) {
    pid_t pid;
    struct rlimit new_limit;
    int result;
    if (argc < 2) {
        return 1;
    }
    pid = atoi(argv[1]);
    new_limit.rlim_cur = RLIM_INFINITY;
    new_limit.rlim_max = RLIM_SAVED_MAX;
    result = prlimit(pid, RLIMIT_CORE, &new_limit, NULL);
    return result;
}

rlimit 相关常量在 /usr/include/bits/resource.h 中有定义。这里 prlimit 的具体定义也可以通过 man 2 prlimit 查看文档。

如果有幸可以用上 util-linux 2.21 以上版本(http://www.kernel.org/pub/linux/utils/util-linux/),那么直接就会有一个 prlimit 命令可用。那就非常方便了。

但是如果内核版本较低,没有这功能又怎么办呢?其实还可以通过万能的 gdb 钩进进程调用 setrlimit 来实现。于是,可以包装一个脚本出来:

#!/bin/bash

f="0"
while getopts ":c:d:e:f:i:l:m:n:p:q:r:s:t:u:v:x" opt; do
  if [ "$f" == "1" ]; then
    echo "too many arguments" >&2;
    exit 1
  fi
  f="1"
  v="$OPTARG"
  case $opt in
  c) r=4  ;; # RLIMIT_CORE       core file size
  d) r=2  ;; # RLIMIT_DATA       data seg size  
  e) r=13 ;; # RLIMIT_NICE       scheduling priority 
  f) r=1  ;; # RLIMIT_FSIZE      file size
  i) r=11 ;; # RLIMIT_SIGPENDING pending signals
  l) r=8  ;; # RLIMIT_MEMLOCK    max locked memory
  m) r=5  ;; # RLIMIT_RSS        max memory size
  n) r=7  ;; # RLIMIT_NOFILE     open files
  q) r=12 ;; # RLIMIT_MSGQUEUE   POSIX message queues
  r) r=14 ;; # RLIMIT_RTPRIO     real-time priority
  s) r=3  ;; # RLIMIT_STACK      stack size
  t) r=0  ;; # RLIMIT_CPU        cpu time
  u) r=6  ;; # RLIMIT_NPROC      max user processes
  v) r=9  ;; # RLIMIT_AS         virtual memory
  x) r=10 ;; # RLIMIT_LOCKS      file locks

  ?) echo "bad argument $opt" >&2; exit 1 ;;
  esac
done

shift $(($OPTIND - 1))

if echo "$v" | grep -q -E "^\-?[0-9]+$"; then
  true
else
  echo "bad rlimti value $v" >&2
  exit 2
fi

if [ `echo "$v==-1 || ($v>=0 && $v<1048576)"|bc` == '0' ];then
  echo "bad rlimti value $v" >&2
  exit 2
fi

pid=$1
bin=`readlink /proc/$pid/exe`
if [ -z "$bin" ]; then
  echo "process $pid not found" >&2
  exit 3
fi

cmd='
set $rlim=&{-1ll,-1ll}
print getrlimit('$r',$rlim)
set *$rlim[0]='$v'
print setrlimit('$r',$rlim)
quit
'

result=`echo "$cmd" | gdb $bin $pid 2>/dev/null | grep '(gdb) \$2 ='`
result="${result##*= }"
exit $result

sudo ./rlimit.sh -c -1 12345

就可以 12345 放开进程的 core dump

参数同 ulimit ,-1 表示 unlimited。不过用这种方法,只能调整 soft rlimit ,而且没法超过原有的 hard rlimit。

修改以后,可以通过 cat /proc/<pid>/limits 查看效果

补充(感谢zolker):
对于 redhat 系,如 centos 6.2 之后可以通过

echo -n 'Max processes=10240:10240' >/proc/<pid>/limits

来修改。这里的 limit 的名称就是

cat /proc/<pid>/limits

里看到的

, , , , ,

MongoDB Sharding 机制分析

Filed in 技术 1 Comment

MongoDB 是一种流行的非关系型数据库。作为一种文档型数据库,除了有无 schema 的灵活的数据结构,支持复杂、丰富的查询功能外,MongoDB 还自带了相当强大的 sharding 功能。

要说 MongoDB 的 sharding,首先说说什么是 sharding。所谓 sharding 就是将数据水平切分到不同的物理节点。这里着重点有两个, 一个是水平切分,另一个是物理节点。一般我们说数据库的分库分表有两种类型。一种是水平划分,比如按用户 id 取模,按余数划分用户的数据,如博客文章等;另一种是垂直划分,比如把用户信息放一个节点,把文章放另一个节点,甚至可以把文章标题基本信息放一个节点,正文放另一个节点。sharding 指的是前一种。而物理节点,主要是和如 mysql 等提供的表分区区分。表分区虽然也对数据进行了划分,但是这些分区仍然是在同一个物理节点上。

那么,为什么要使用 sharding 呢?sharding 解决了什么问题,带来了什么好处呢?不少人都经历过自己的网站、应用由小到大,用户越来越多,访问量越来越大,数据量也越来越大。这当然是好事。但是,以前一个服务器就可以抗下的数据库现在不行了。开始还可以做做优化,再加个缓存。但是再后来,无论如何都不是一个服务器能承受了。数据量也会很快超过服务器的硬盘容量。这时候,就不得不进行拆分,做 sharding 了。这里,sharding 可以利用上更多的硬件资源来解决了单机性能极限的问题。此外,将数据进行水平切分后,还会减小每个索引的体积。由于一般数据库的索引都是 B树 结构,索引体积减小后,索引深度也会随之减小,索引查找的速度也会随之提高。对于一些比较费时的统计查询,还可以由此把计算量分摊到多个机器上同时运算,通过分布式来提高速度。

虽然 sharding 有很多好处,但是传统数据库做 sharding 会遇到很多麻烦事。首先是扩容和初始化的问题。比如,原来按用户 id 模 5,分了 5 个节点,后来随着数据增长,这 5 个也不够用了,需要再增加。这时候如果改成 10 个,则至少要挪动一半数据。如果不是整倍数,比如扩展到 7 个节点,那绝大部分数据都会被挪一遍。对于已经做了 sharding 的大规模数据库来说,这是一件相当可怕的事情。而且在数据迁移期间,通常都无法继续提供服务,这将造成很长时间的服务中断。对从一个未做 sharding 的数据库开始创建也是同样。如果使用虚拟节点,比如将数据划分成 1000 个虚拟节点,然后通过映射关系来找到对应的物理节点,可以有所改善。但仍然无法避免迁移过程中的服务中断。另一个麻烦事是数据路由。数据拆分以后,应用程序就需要去定位数据位于哪个节点。还需要将涉及多个节点的查询的结果合并起来。这个工作如果没有使用数据库中间件的话,就需要花不少功夫自己实现,即使使用了中间件,也很难做到透明。由于关系型数据库功能的复杂性,很多功能在 sharding 上将无法正常使用。比如 join 、事务等。因此会造成应用层的大量修改测试工作。

sharding 会有这许多麻烦事,那么 MongoDB 的 sharding 又如何呢?

MongoDB 的 sharding 的特色就是自动化。具体体现为可以动态扩容、自动平衡数据、以及透明的使用接口。可以从一个普通的 replica set,或者单个实例平滑升级,可以动态增加删除节点,响应数据快速增长。可以自动在节点间平衡数据量,避免负载集中在少数节点,而在这期间不影响数据库读写访问。对客户端,可以使用完全相同的驱动,大部分功能可用,基本不需要更改任何代码。

MongoDB 的 sharding 有如此强大的功能,它的实现机制是怎样的呢?下图就是 MongoDB sharding 的结构图。

从图中可以看出,MongoDB sharding 主要分为 3 大部分。shard 节点、config 节点和 config 节点。对客户端来说,直接访问的是 图中绿色的 mongos 节点。背后的 config 节点和 shard 节点是客户端不能直接访问的。mongos 的主要作用是数据路由。从元数据中定位数据位置,合并查询结果。另外,mongos 节点还负责数据迁移和数据自动平衡,并作为 sharding 集群的管理节点。它对外的接口就和普通的 mongod 一样。因此,可以使用标准 mongodb 客户端和驱动进行访问。mongos 节点是无状态的,本身不保存任何数据和元数据,因此可以任意水平扩展,这样任意一个节点发生故障都可以很容易的进行故障转移,不会造成严重影响。

其中蓝色的 shard 节点就是实际存放数据的数据节点。每个 shard 节点可以是单个 mongod 实例,也可以使一个 replica set 。通常在使用 sharding 的时候,都会同时使用 replica set 来实现高可用,以免集群内有单个节点出故障的时候影响服务,造成数据丢失。同时,可以进一步通过读写分离来分担负载。对于每个开启 sharding 的 db 来说,都会有一个 默认 shard 。初始时,第一个 chunk 就会在那里建立。新数据也就会先插入到那个 shard 节点中去。

图中紫色的 config 节点存储了元数据,包括数据的位置,即哪些数据位于哪些节点,以及集群配置信息。config 节点也是普通的 mongod 。如图所示,一组 config 节点由 3 个组成。这 3 个 config 节点并非是一个 replica set。它们的数据同步是由 mongos 执行两阶段提交来保证的。这样是为了避免复制延迟造成的元数据不同步。config 节点一定程度上实现了高可用。在一个或两个节点发生故障时,config 集群会变成只读。但此时,整个 sharding 集群仍然可以正常读写数据。只是无法进行数据迁移和自动均衡而已。

config 节点里存放的元数据都有些啥呢?连上 mongos 后,

use config; show collections

结果是

settings
shards
databases
collections
chunks
mongos
changelog

还可以进一步查看这些东西的数据。这个 config 库就是后端 config 节点上的数据的映射,提供了一个方便的读取元数据的入口。这些 collection 里面都是什么呢? settings 里是 sharding 的配置信息,比如数据块大小,是否开启自动平衡。shards 里存放的是后端 shard 节点的信息,包括 ip,端口等。databases 里存放的是数据库的信息,是否开启 sharding,默认 shard 等。collections 中则是哪些 collection 启用了 sharding,已经用了什么 shard key。chunks 里是数据的位置,已经每个 chunk 的范围等。mongos 里是关于 mongos 的信息,changelog 是一个 capped collection,保存了最近的 10m 元数据变化记录。

mongodb sharding 的搭建也很容易。简单的几步就能完成。

先启动若干 shard 节点

mongod --shardsvr

启动 3 个 config 节点

mongod --configsvr

启动 mongos

mongos --configdb=192.168.1.100, 192.168.1.101, 192.168.1.102

这里,–shardsvr 参数只起到修改默认端口为 27018 作用,–configsvr 则修改默认端口为 27019 以及默认路径为 /data/configdb。此外并没有什么直接作用。实际使用时,也可以自己指定端口和数据路径。此外,这两个参数的另一个作用就是对进程进行标记,这样在 ps aux 的进程列表里,就很容易确定进程的身份。–configdb 参数就是 config 节点的地址。如果更改了默认端口,则需要在这里加上。

然后我们把数据节点加入集群:在 mongos 上运行

use admin
sh.addShard(’ [hostname]:[port]’)

如果使用的事 replicaSet,则是

use admin
sh.addShard(’replicaSetName/,,’)

接着就是启用 sharding 了。

sh.enableSharding(dbname)
sh.shardCollection(fullName, key, unique)

这样就可以了。还是很简单的吧。如果 collection 里有数据,则会自动进行数据平衡。

之前说过,mongodb 的 sharding 把数据分成了数据块(chunk)来进行管理。现在来看看 chunk 究竟是怎么回事。在 mongodb sharding 中,chunk 是数据迁移的基本单位。每个节点中的数据都被划分成若干个 chunk 。一个 chunk 本质上是 shard key 的一个连续区间。chunk 实际上是一个逻辑划分而非物理划分。sharding 的后端就是普通的 mongod 或者 replica set,并不会因为是 sharding 就对数据做特殊处理。一个 chunk 并不是实际存储的一个页或者一个文件之类,而是仅仅在 config 节点中的元数据中体现。mongodb 的sharding 策略实际上就是一个 range 模式。

如图,第一个 chunk 的范围就是 uid 从 -∞ 到 12000 范围内的数据。第二个就是 12000 到 58000 。以此类推。对于一个刚配置为 sharding 的 collection ,最开始只有一个 chunk,范围是从 -∞ 到 +∞。

随着数据的增长,其中的数据大小超过了配置的 chunk size,默认是 64M 则这个 chunk 就会分裂成两个。因为 chunk 是逻辑单元,所以分裂操作只涉及到元数据的操作。数据的增长会让 chunk 分裂得越来越多。这时候,各个 shard 上的 chunk 数量就会不平衡。这时候,mongos 中的一个组件 balancer 就会执行自动平衡。把 chunk 从 chunk 数量最多的 shard 节点挪动到数量最少的节点。

最后,各个 shard 节点上的 chunk 数量就会趋于平衡。当然,balance 不一定会使数据完全平均,因为移动数据本身有一定成本,同时为了避免极端情况下早晨数据来回迁移,只有在两个 shard 的 chunk 数量之差达到一定阈值时才会进行。默认阈值是 8 个。也就是说,默认情况下,只有当两个节点的数据量差异达到 64M * 8 == 256M 的时候才会进行。这样就不用对刚建好的 sharding ,插入了不少数据,为什么还是都在一个节点里感到奇怪了。那只是因为数据还不够多到需要迁移而已。

在数据迁移的过程中,仍然可以进行数据读写,并不会因此而影响可用性。那么 mongodb 是怎么做到的呢?在数据迁移过程中,数据读写操作首先在源数据节点中进行。待迁移完毕后,再将这期间的更新操作同步到新节点中去。最后再更新 config 节点,标记数据已经在新的地方,完成迁移。只有在最后同步迁移期间的操作的时候,需要锁定数据更新。这样就讲锁定时间尽可能缩小,大大降低数据迁移对服务的影响。

mongodb 的 sharding 和传统 sharding 的最大区别就在于引入了元数据。看似增加了复杂度,并增加了一些额外的存储,但是由此带来的灵活性却是显而易见的。传统的 sharding 本质上是对数据的静态映射,所有那些数据迁移的困难都是由此而来。而引入元数据以后,就变静态映射为动态映射。数据迁移就不再是难事了。从而从根本上解决了问题。另一方面,用元数据实现 chunk 则降低了实现难度,后端节点仍然可以使用原有的技术。同时,因为不需要对后端数据进行变动,也使部署迁移变得更容易,只需要另外加上 mongos 节点和 config 节点即可。

再说说数据路由功能。mongos 的最主要功能就是作为数据路由,找到数据的位置,合并查询结果。来看看它是如何处理的。如果查询的条件是 shard key ,那么 mongos 就能从元数据直接定位到 chunk 的位置,从目标节点找到数据。

如果查询条件是 shard key 的范围,由于 chunk 是按 shard key 的范围来划分的,所以 mongos 也可以找到数据对应 chunk 的位置,并把各个节点返回的数据合并。

如果查询的条件不是任何一个索引,原来的全 collection 遍历仍然不可避免。但是会分发到所有节点进行。所以,还是可以起到分担负载的作用。

如果查询的条件是一个索引,但不是 shard key,查询也会被分发到所有节点,不过在每个节点上索引仍然有效。

如果是按查询 shard key 进行排序,同样由于 chunk 是一个 shard key 的范围,则会依次查询各 chunk 所在节点,而无需返回所有数据再排序。如果不是按 shard key 排序,则会在每个节点上执行排序操作,然后由 mongos 进行归并排序。由于是对已排序结果的归并排序,所以在 mongos 上不会有多少压力,查询结果的游标也会变成在每个节点上的游标。并不需要把所有数据都吐出来。

从上面可以看到,对 sharding 集群来说,shard key 的选择是至关重要的。shard key 其实就相当于数据库的聚簇索引,所以选择聚簇索引的原则和选择 shard key 的原则是差不多的。同样, shard key 一旦设定就无法再更改,所以,选择的时候就要谨慎。shard key 的选择主要就这么几点。

首先,shard key 的值要是固定的,不会被更改的。因为一旦这个值被更改,就有可能会从一个节点被挪动到另一个节点,从而带来很大的开销。

第二,shard key 要有足够的区分度。同样因为 chunk 是一个 shard key 的范围,所以 shard key 相同的值只能位于同一个 chunk 。如果 shard key 相同的值很大,致使一个 chunk 的大小超过了 chunk size,也无法对 chunk 进行分裂,数据均衡。同时,和一般的数据库索引一样,更好的区分度也能提高查询性能。

第三,shard key 还要有一定的随机性而不是单向增长。单向增长的 shard key 会导致新插入的数据都位于一个 chunk 中,在在某一个 shard 节点中产生集中的写压力。所以,最好避免直接使用 _id ,时间戳 这种单向增长的值作为 shard key。

mongodb 的 sharding 有很多优势,但是也同样有其局限性。

首先,mongodb 只提供了 range 模式的 sharding。这种模式虽然可以对按 shard key进行 range 查询、排序进行优化,但是也会造成使用单向增长的值时,写入集中的结果。

第二,启用了 sharding 之后,就无法保证除 shard key 以为其他的索引的唯一性。即使设为 unique,也只是保证在每个节点中唯一。有一个办法是,把索引设为 {<shard_key>:1, <unique_key>:1} 。但是这样并不一定满足业务逻辑需求。

第三,启用 sharding 后,无法直接使用 group() 。但是可以用 map reduce 功能作为替代。

第四,虽然数据迁移操作对读写影响很小,但是这个过程需要先把数据从磁盘中换入内存才能进行,所以可能会破坏热数据缓存。此外,数据迁移也还是会增大 io 压力,所以可以考虑平时关闭自动平衡,在凌晨压力小的时候再进行。

最后,config 节点的元数据同步对时钟准确性要求比较高,一旦各 config 时钟误差大了,就会出现无法上锁,从而无法更改,导致数据集中。因此 ntp 时钟同步时必不可少的。

在这里再说一下 sharding 集群的备份问题。由于后端数据节点仍然是普通的 mongod 或 replica set,所以备份其实和原先差不多。只是需要注意的是,备份前需要停止自动平衡,保证备份期间 sharding 的元数据不会变动,然后备份 shard 节点和 config 节点数据即可。

P.S. 这篇东西是十月份我在 thinkinlamp 第三届数据库大会上的 topic 的内容的整理。

,

​puppet 系统配置自动化解决方案

Filed in 技术 1 Comment

这是一篇几个月前被枪毙的文章。当然,确实写太浅了一些,就是一入门。因为我对 puppet 本来了解得也不深入。这会想起了还有这篇文章,姑且放出来。高手们还是绕路吧。:-)

相信做过运维的朋友都会有这样的体会:把一个新的服务器从刚装好系统的状态配置到可以运行应用程序,是个挺麻烦的过程。就拿一个运行 nginx + php 的 web 服务器为例,可能需要部署 ssh 公钥,设置用户 sudo 权限,关闭密码登录、root 远程登录,配置 iptables 规则。然后安装所需版本的 nginx、 php 到规范的路径,不能搞错版本,以免缺失所需特性或者造成冲突,还要安装应用所需要的 php 扩展比如 gd 之类。然后是用于监控的客户端程序,比如 nagios 的 nrpe ,或者 zabbix_agent,用于日志轮转的 cronolog 等等。最后还得记得修改 ulimits 、tcp 相关的内核参数。然后还得一一验证所有的设置是否正确。如果是部署了新的东西或者更新了配置,还得写一下安装文档,这样下回安装的时候才不会遗漏什么编译 选项、软件包,或者少设置了系统参数导致故障。有人请假或则离职的时候,别人也才能接替。需要部署的还不会仅仅是 web 服务器,还有 lvs 、mysql、memcache,也许还会有 redis、sphinx 等等等等。有软件包需要升级,也得对所有用到的机器都升级一遍。更要命的是,往往需要配置的还不是一台机器,而是十几台乃至上百台。

这里 可以看出,系统配置本身是件很繁琐的事情。需要考虑到很多方面的事情,任何一个地方出了纰漏就可能导致故障或者埋下隐患。手动来做很容易出错,也很不够敏 捷高效,难以快速响应需求。为了解决这些问题,我也曾经用 bash 做过一些脚本,来实现部署和系统设置的自动化,但是受到 bash 的表达能力的限制,脚本的编写测试维护并不轻松。我们碰到的麻烦别人也会碰到。所以就有了自动化好在现在有了一个好用的工具来解决这些问题。puppet 就是这样一个功能强大的系统配置集群管理工具。puppet 分为 master 端和 agent 端,可以实现分布式分发,有强大的配置管理功能,可以实现自动化分发文件、安装软件包、执行命令、添加系统用户、设置 crontab 等等。它的配置文件是一种表达能力强的 DSL,可读性好、容易复用,且本身就可以作为很好的文档,这就免除了维护文档的负担,也避免了文档过时的问题。puppet 还使用了 ssl 来保证通讯的安全性,防止敏感的配置信息泄露。支持集群化,以实现大批量主机并行更新维护。引入的 factor 还实现了针对不同系统、不同发行版、不同环境的针对性设置。以及很多方便强大的功能。

我们先看看如何安装 puppet 。最方便的方法是使用包管理器。对于 centos 等 redhat 系发行版,可以通过 yum 来安装。先把 puppet 的官方源加入到系统中:

# cat > /etc/yum.repos.d/puppet.repo << EOF
[puppet-dependencies]
name=puppet dependencies
baseurl=http://yum.puppetlabs.com/el/\$releasever/dependencies/\$basearch/
gpgcheck=1
gpgkey=http://yum.puppetlabs.com/RPM-GPG-KEY-puppetlabs

[puppet-products]
name=puppet dependencies
baseurl=http://yum.puppetlabs.com/el/\$releasever/products/\$basearch/
gpgcheck=1
gpgkey=http://yum.puppetlabs.com/RPM-GPG-KEY-puppetlabs

EOF

然后

# yum install puppet  

就会自动安装好 puppet。从所列出依赖的软件包可以看出, puppet 是用 ruby 实现的。对于 debian 系发行版,也可以在 apt.puppetlabs.com 中找到相应的源和 gpg key。

puppet 分为 master 和 agent 。找两台机器或者两个虚拟机,一台作为 master,一台作为 agent ,两端都安装好后就可以开始配置了。

首先配置 master 。先在 master 端运行一下

# puppet master  

初次运行会生成puppet 用户, 在 /etc/puppet 目录下生成默认配置目录结构并在 /var/lib/puppet 生成数据文件。如果提示 permisiton denided ,可以试试 chown puppet:puppet /var/lib/puppet/run 。
然后我们就可以开始写第一个配置文件了。在 /etc/puppet/manifests 目录下建一个 site.pp ,这是 puppet master 的主配置文件。输入

package { "bison":
        ensure=>"installed",
}

exec { "puppet test":
        command=>"/bin/touch /tmp/puppet-test",
}

第一个配置会使 agent 端确保编译 php 所必须的软件包 bison 已经安装好。对于不同的系统,会使用各自的包管理器来安装。第二个配置会在 agent 端执行 /bin/touch /tmp/puppet-test 。
然后配置客户端。先编辑 /etc/hosts,加入 master 的 ip,如:

192.168.1.101 puppet 

在客户端运行一下

# puppet agent --test 

初次运行也同样会生成客户端的相应文件,然后就会去连接 master 端执行任务。此时会提示

warning: peer certificate won't be verified in this SSL session exiting; no certificate found and waitforcert is disabled 

这表示 agent 需要认证。因为 puppet 使用了 ssl 来保证安全,并需要 agent 经过 master 认证才能够访问配置。到 master 端执行一下

# puppet cert list 

会列出待认证的 agent 列表。这里可以看到 agent 的主机名。如

puppet-agent-test-01 (66:62:5C:84:B0:23:73:FB:80:7C:89:48:4C:A6:AF:53) 

然后可以使用

# puppet cert sign puppet-agent-test-01 

就能完成认证。如果觉得直接使用主机名不够灵活,也可以在运行 agent 时使用 --certname=认证名 来指定。在 agent 端再试一次,这回就可以看到,agent 已经开始干活了。看看 bison 工具是否安装好了,再看看 /tmp 目录下是否生成了 /tmp/puppet-test 文件。

需要注意的是,一个主机可以使用多个不同的certname,但一个certname只能被一台主机使用。如果原有的certname需要移动到另一个主机上使用,就需要在master端先 puppet cert clean "认证名" 来清除原有数据。所以,certname应当尽量保持全局唯一。

这里 agent 使用的 --test 让 agent 不以服务方式运行,只执行一次,并输出详细信息。去掉这个参数,puppet agent 就会以服务方式在后台运行,默认每 30 分钟连接一次服务器更新配置。可以用 puppet help master、puppet help agent 查看更多选项。

刚才是把所有的配置都写在了 sites.pp 文件里。在配置项增多,维护的项目增多以后,就会变得过于庞大而难以维护。所以就需要把配置分到不同的模块中去,以模块化的方式来管理配置。
puppet 的模块放在 /etc/puppet/modules 下。模块的目录结构如下图所示:

modules/
|-- test
    |-- files
    |   `-- test.txt
    `-- manifests
        |-- init.pp
        `-- test.pp 

在这个例子里,定义了一个 test 模块。其中 files 目录中用于安放该模块所需分发的文件,manifests 目录中是该模块的配置文件。其中 init.pp 是每个模块的主配置文件。内容通常为 import "*" ,来载入该模块的其他配置文件。我们在 files 目录中加入一个 test.txt 文件,并把之前 site.pp 中的内容挪到 test.pp 中,再加入分发文件的配置,定义成一个类:

class test1 {
package { "bison":
        ensure=>"installed",
}
exec { "puppet test":
        command=>"/bin/touch /tmp/puppet-test",
}
file { "/tmp/test.txt":
        ensure => "present",
        source => "puppet:///modules/test/test.txt"
}
}

site.pp 中删除原有的配置,加入 import "test" ,把 test 模块加载进来,然后加入 include "test1" ,应用 test1 类的配置。Include语句也可以再class内使用。以在一个class中复用另一个class的配置。现在,我们在 agent 端再运行一次 puppet agent --test ,agent 还是会照常工作,但是配置已经分到模块中了。实践中,会把每个配置的项目建立一个模块,比如:nginx、php 等等。再看看 /tmp 目录,会发现 master 端的 test.txt 文件已经下载回来。puppet 已经完成了文件分发的工作。module 中的 files 通常用于分发配置文件,把软件包的配置文件集中管理。

file配置也可以用来创建目录。只要使用 ensure => "directory" 即可。如:

file { "/tmp/testdir":
        ensure => "directory",
}

到目前为止,我们只使用了一个agent,实际环境中,会有许多台需要不同配置的 agent 。这就需要对不同的 agent 应用不同的配置。在 sites.pp 中把 include test1 替换成针对特定节点的配置:

import "test"
node "puppet-agent-test-01" {
        include "test1"
}

这里的主机名可以用 "," 分隔,指定多个主机,也可以用类似 /puppet-agent-test-.*/ 这样的正则表达式来灵活匹配。为了测试配置是否生效,我们可以修改一下之前的配置,再运行一下 agent。还可以再加入几台 agent 试试应用不同的节点的不同配置。

之前加入 agent 时,需要在 master 端手动为 agent 认证。在客户端众多,或者需要完全自动化的时候,可以配置自动签名。当然,前提是能通过别的途径比如 iptables 限制访问 master 的来源,或者是在可信的内网环境下。

在 /etc/puppet 目录中添加 autosign.conf 中输入agent 认证名的模式,如 *.test.net 。需要注意的是,这里必须使用类似泛域名通配符的方式。也就是说,* 只能出现在前面,而不允许 test.* 这样的形式。所以要应用自动签名,在规划 agent 的认证名的时候就要注意这一点。现在,我们用 puppet agent --test --certname="agent01.test.net" 试试。这回就不再需要手动认证,直接就能执行 master 分发的配置任务了。

实际应用puppet时,会把puppet的配置文件,以及要分发的软件包的配置文件都加入到svn等源代码管理中。但是我们也会需要用puppet来分发一些我们自己编译打包的软件包等二进制文件。这些二进制文件并不适合放进源代码管理中。另外,需要用puppet分发的证书、密钥等敏感信息也不适合放入。这时,使用模块的文件就不太方便。好在puppet的文件服务器也是可以配置的。建立puppet文件服务器配置文件:/etc/puppet/fileserver.conf,输入

[modules]
    allow *

[files]
    path /data/puppet
    allow * 

这就定义了一个名为files的额外文件服务挂载点,位于 /data/puppet。也放一个 test.txt 在里面,然后就可以使用

 
file { "/tmp/test2.txt":
        source => "puppet:///files/test.txt"
}

来分发了。这里 puppet:// 是协议名,后面的路径 /files/test.txt 的第一部分是挂载点名称。之前使用的 modules 这个特殊的挂载点名是指向各个模块的文件。如 /modules/test/test.txt 就是test模块下files目录中的test.txt文件。而 /files/test.txt 就是 files 挂载点对应目录下的 test.txt 文件。之后,我们就可以把这些二进制文件等用别的途径部署到自定义挂载点上。

之前我们使用的file、exec、package 在puppet中都被称为资源。每一种资源都有许多参数可以设置。对于 file 资源,可以用 ensure => "link", target => "目标路径" 来建立软链接,用mode=>0644来设置文件访问权限。对于exec资源,可以用cwd参数来设置当前路径,用creates参数来设置执行命令创建的路径,可以用于防止重复执行命令。对于所有的资源类型,都有一些共有的参数,称为元参数。其中最常用的就是require参数。这个参数指定了这个资源所依赖的资源。实际应用中,不同的任务间会有依赖关系。比如安装软件包需要先创建好目标路径的目录,需要先安装好所需的依赖软件包,这就可以用require选项来实现。比如:

exec { "install sth.":
  cwd => "/opt/some_package",
  exec => "/bin/tar -xzvf /path/to/package.tar.gz",
  require => File["/opt/some_package"],
}
file { "/opt/some_package":
  ensure => "directory",
} 

任务 Exec["install sth."] 就会在任务 File["/opt/some_package"] 完成后运行。这里,对于每个类型的资源,可以用“,”分隔;如果要指定多种类型的资源,也可以写成列表形式。如:

require => [ File["/path/to/file1", "/path/to/file2"], Package["package1] ] 

还有一个与require相反的参数before,可以指定该任务必须在哪些任务前完成。

puppet还提供了多种资源类型来完成不同的任务。比如可以用cron类型来管理定时任务,用host类型来设置hosts文件等。

至此,已经简单介绍了puppet 的基本功能设置,可以针对不同主机安装执行不同的所需任务。puppet 还有更多强大的功能,您可以参照 puppet 的官方文档,各取所需,在实践中学习应用。应用puppet,把原先繁琐的系统配置过程自动化了,需要部署一台新的服务器时,只需要在初始化好puppet后,执行一次 puppet agent --test,剩下的就交给它来干了。既省时省力也不会出错。机械化的操作就应当交给机器来做,这样才能把人的精力省出来做更有价值的事情。

调试一个 xdebug 造成的 Segmentation fault

Filed in 技术 1 Comment

在调试一个用了 solr 扩展的 php 代码时候,发现调用 query 会发生 Segmentation fault 。于是想知道是从什么地方来的错误。开始觉得是 solr 扩展的问题。但是只有一个 Segmentation fault 不好定位。所以就先搞出 coredump 出来。先写一个简单的复现程序 test.php ,就是简单地初始化好 solr ,然后调用一次 query ,然后打开 coredump,运行一下。

ulimit -c unlimited
php test.php

这就在当前目录下产生了一个 core 文件。然后就可以上 gdb 了

gdb core test.php
.....
(gdb) bt
#0  zend_memrchr (type=1, error_filename=, error_lineno=34, format=, args=)
    at /usr/local/include/php/Zend/zend_operators.h:291
#1  xdebug_error_cb (type=1, error_filename=, error_lineno=34, format=, args=)
    at /home/xiezhenye/xdebug-2.2.1/xdebug_stack.c:626
#2  0x000000000076ac0e in zend_error_va (type=718182544, file=0xa 
, lineno=4294967295, format=0x10
) at /home/xiezhenye/php-5.4.7/Zend/zend_exceptions.c:767 #3 0x000000000076ad8e in zend_exception_error (exception=0x7fcc2ace1600, severity=1) at /home/xiezhenye/php-5.4.7/Zend/zend_exceptions.c:807 #4 0x0000000000752aba in zend_execute_scripts (type=8, retval=, file_count=3) at /home/xiezhenye/php-5.4.7/Zend/zend.c:1310 #5 0x00000000006f72fd in php_execute_script (primary_file=) at /home/xiezhenye/php-5.4.7/main/main.c:2473 #6 0x00000000007f98ac in do_cli (argc=, argv=) at /home/xiezhenye/php-5.4.7/sapi/cli/php_cli.c:988 #7 0x00000000007f9fe4 in main (argc=, argv=) at /home/xiezhenye/php-5.4.7/sapi/cli/php_cli.c:1364 (gdb)

发现调用栈里没有 solr 的东西,最可疑的倒是 xdebug_error_cb 。看起来可能是 xdebug 的问题。于是先禁用掉 xdebug ,果然一切就正常了,输出了 solr 的爆长的异常信息。

于是去看看 xdebug 的那段代码 xdebug_stack.c 626 行。附近几行是这样的:

 623                                 /* find first new line */
 624                                 p = strchr(buffer, '\n');
 625                                 /* find last quote */
 626                                 p = ((char *) zend_memrchr(buffer, '\'', p - buffer)) + 1;
 627                                 /* Create new buffer */
 628                                 tmp_buf = calloc(p - buffer + 1, 1);
 629                                 strncpy(tmp_buf, buffer, p - buffer );

zend_memrchr 看起来是个和 strrchr 差不多的函数,在字符串里查找字符。第三个参数是字符串的长度。从这里出内存错误,多半是访问越界。这个长度里,p 的来源是 strchr 。如果 buffer 里没有 ‘\n’,又没以 ‘\0′ 结尾就有可能造成返回的 p 不在 buffer 的正常范围内,就可能造成访问越界。但是什么情况下回出现呢?查看 buffer 的来历的时候,看到有这么一行。

 550         buffer_len = vspprintf(&buffer, PG(log_errors_max_len), format, args);

log_errors_max_len 是 php.ini 里关于错误信息长度的选项。于是想到,会不会是那个爆长的 solr 错误信息引发的。

写一个测试代码:

<?php
throw new Exception("long message ".str_repeat('.', 10240));

带着 xdebug 运行一下,果然又出现 Segmentation fault 。

已提交 bug:http://bugs.xdebug.org/view.php?id=885
对应 xdebug 版本 2.2.1

php 中运行外部程序的一个潜在风险

Filed in 技术 2 Comments

php 中有 exec system popen 等一系列运行外部程序的函数。在 web 环境中使用这些函数的时候,即使控制好了权限,保证了被执行程序本身的安全,还可能有另外的潜在风险。

php 的这些函数实际上是使用了 popen 函数。popen 利用了 vfork 来启动一个 shell 子进程来执行命令。但是 popen 并没有在子进程中关闭原有的进程的文件描述符。这样子进程也会占有这些文件描述符,即使它们并不需要,如果子进程长时间运行,还会导致这些资源没法释放。

比如在 php-fpm 环境中,如果在子进程长时间运行时 php-fpm 崩溃,或者手动停止服务,监听的端口 9000 所对应的文件描述符还会被子进程共享。此时想重新启动 php-fpm 也会因为端口被占用而失败。

比如运行如下程序

<?php
exec("sleep 1000;");

然后

killall php-fpm

杀死 php-fpm 后,

netstat -lntp

会看到,9000 端口被一个 sh 进程占用。

...
tcp        0      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      3935/sh
...

查看这个 sh 会发现这正是执行 sleep 的那个进程。

...
nobody    3935  0.0  0.0   4272   580 ?        S    11:06   0:00 sh -c cd '/usr/share/nginx/www' ; sleep 1000
...

再探 goroutine

Filed in 技术 4 Comments

之前写过一篇 go 语言并发机制 goroutine 初探,后来对 go 如何跟踪 syscall 调用返回产生了兴趣,研究后发现,之前的一些看法是错误的。

问题是这么发现的。

// par.go
package main

import (
"fmt";
"runtime"
"strconv"
"time"
)

func main() {
        runtime.GOMAXPROCS(2)
        ch := make(chan int)
        n := 1000
        for i := 0; i < n; i++ {
                task(strconv.Itoa(i), ch, 100)
        }
        fmt.Printf("begin\n")
        for i := 0; i < n; i++ {
                <-ch
        }
}

func task(name string, ch chan int, max int) {
        go func() {
                i:= 1
                for i <= max {
                        fmt.Printf("%s %d\n", name, i)
                        i++
                }
                ch <- 1
        }();
}

这么一个程序,运行 ./par | less ,然后查看 /proc/<pid>/tasks,或者用类似的 pstree -p <pid> 。原来指望只有很少的线程数。结果却是 1002 个。看起来似乎是实实在在地为每个 goroutine 启动了一个线程。然后又用 strace -f ./par 2>&1 | less 跟踪,也发现了大量的 clone 系统调用。也就是说,goroutine 并不是像我之前认为的,在 cgocall 或者 syscall 的时候进行自动切换,而是使用了线程。同时,这个线程数和 runtime.GOMAXPROCS 也没有直接关联。在这个情况下,虽然 runtime.GOMAXPROCS 设为了 2 ,但是最后照样用了 1000 多个线程。但是 strace -f ./par 直接运行,此时跟踪线程数,最多就只有几十个。看来和 less 也有关系。

在 golang 的邮件列表里提问了解到,goroutine 在遇到阻塞性的系统调用,比如 Read ,或者 cgo 调用,会启用一个线程来处理这些调用。想想也和结果对应上了。因为使用了 less ,在显示完一屏幕,并且管道缓冲也被填满后,fmt.Printf 底层对应的 Write 系统调用全部被阻塞。而由于每一个系统调用都需要一个线程来处理,于是就有了 1000 多个线程。由此看来,goroutine 也并不是之前想象中的那么神奇。同时邮件列表里也提到,对于 net 包,还是使用了异步 io 系统调用,因此在网络应用中并不会由于网络 io 速度慢造成阻塞而产生大量线程。看了 net 包的源代码下的 netfd(net/fd*.go) ,确实如此。

go 语言要避免大量线程产生的切换开销,用类似 coroutine 的方式,还是得结合异步 io 。但是目前只在网络 io 上实现了这点。对于其他的 io,比如文件系统,仍然会由于阻塞而产生线程。如果应用中需要使用文件 io,就得使用生产者消费者模式来减少线程数量,或者可以考虑利用 netfd 的代码来实现一个其他类型 io 的异步包装(当然功能上会有一些限制)。

,

调试一个 Bus error 错误

Filed in 技术 Leave a comment

遇到一部分主机运行 php 的时候报 Bus error 错误,直接退出。

# /path/to/php
Bus error

于是用 strace 跟踪了一下,看看到底是怎么回事。

# strace /path/to/php
...
...
...
open("/path/to/php/lib/php/extensions/no-debug-non-zts-20090626/mongo.so", O_RDONLY) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0`\270\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0644, st_size=32636, ...}) = 0
mmap(NULL, 2309488, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x2b1e8e689000
mprotect(0x2b1e8e6ba000, 2097152, PROT_NONE) = 0
mmap(0x2b1e8e8ba000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x31000) = 0x2b1e8e8ba000
--- SIGBUS (Bus error) @ 0 (0) ---
+++ killed by SIGBUS +++

strace 的最后几行如上面所示,进程受到了一个 SIGBUS 信号,看起来是 mmap 引起的。从这几行里看,是 php 在加载 mongo 扩展 mongo.so 时出的错。搜索了一下,mmap 映射内存的时候,在映射超出文件大小范围的内存空间时,可能引发这个 SIGBUS 。就上面的结果看,fstat 的结果表明,文件大小是 32636 字节。而最后那个 mmap 的 offset 参数 0×31000 即 200704,的定位范围直接就超过了文件大小,所以就引发了这个错误。

考虑到这个问题只在部分主机上出现,所以应当不是 php 或者 mongo 扩展本身的 bug 。最后发现是通过 puppet 同步下来的 mongo.so 不完整,比正常的小了很多。重新同步后恢复正常。

由此也可以看出,在加载动态库时是根据 elf 文件中标记的段大小来映射内存的。所以就发生了在文件不完整时 mmap 出错的情况。

, ,

在 mysql 中对特定的库禁用 DDL 语句

Filed in 技术 Leave a comment

mysql 的权限控制功能虽然已经比较强大,但是是基于白名单规则,所以没法做到对除某某库,如 mysql 库以外的所有库分配权限,或者说单独禁用某个库的某些权限。虽然可以一个个库地分配,但是这样毕竟麻烦,尤其是在库的数量会动态变化的情况下。

对于 DDL 语句,也就是 create 、alter 、drop 之类,有一个特殊的办法可以做到。其实也很简单,去掉那个库所在的目录的写权限即可。例如:

chmod a-w mysql

之后,在 mysql 库上执行任意 DDL 语句都会出错。

这方法看着挺恶心,不过也没找到更好的解决办法。

TOP