Redis(一)redis介绍及NIO原理介绍(epoll)

  • Post author:
  • Post category:其他


周老师讲的redis,“全网最权威的讲解”.



前言

几个常识:

  1. 计算机中,数据存在磁盘里,磁盘有两个关键的维度指标:1.寻址,ms级速度;2.带宽,G/M 每秒.

    内存的两个指标:1.寻址,ns级(秒->毫秒->微秒->纳秒),在寻址上磁盘比内存满了10W倍;2.带宽,也比磁盘大很多
  2. I/O buffer:成本问题,如果访问硬盘的时候,都以

    磁盘有磁道和扇区,每个扇区512Byte.

    如果一个磁盘以512字节为单位读写,会使索引成本变大.(每块数据很小的话,需要索引的块就会很多)

    格式化磁盘的时候,有个4K对齐的概念(也可以格式化8K 16K等,看需求),真正使用硬件的时候,并不是以512字节为一次读写量,无论读多少,操作系统都是最少4K从磁盘拿.
  3. 计算机信息系统 2个基础设施:1.冯诺伊曼体系的硬件;2.以太网,tcp/ip的网络

数据存储发展历史:

  1. 很早以前,刚有计算机的时候,数据可以存在文件里,使用grep awk等获取数据.随着文件变大,查询速度会变慢,硬盘I/O成为瓶颈(需要全量遍历)
  2. 这时候关系型数据库出现了,带着两个特点

    1. 从软件从把数据分为一个个的data page,每个data page大小为4K(和磁盘保持一致),但是只有这个并没有什么用.
    2. 索引,也是硬盘中4K的存储模型,从全量数据中抽取一些特征列作为索引,指向原始数据的data page;查询的时候通过B+树,树干(区间和偏移)在内存中,它的叶子节点是索引和数据的data page,减少I/O.

关系型数据库建表的时候必须给出schema,表有多少列(字段),每一列的类型(字节宽度).

写数据时倾向于行级存储,往data page写入数据的时候不管有没有值都会开辟空间,没值的字段也会(用0)占位.这样的好处是未来添加或修改字段的时候,不需要移动数据,直接覆盖写入即可.

数据库的表很大的时候,性能下降?

如果表有索引,增删改会变慢,时间花在索引的维护上.

那查询速度呢?

对于查询的速度,假设内存够大,可以把B+tree的树干存下,B+tree不受影响,查询条件能够命中索引的话,少量查询依然很快;

当查询并发很大,查询的数据散在不同data page上,会受硬盘带宽影响,速度变慢.因为需要把每个数据块以此从硬盘读到内存中

  1. 内存级别的关系型数据库

    当年SAP出的HANA,内存级别的关系型数据库,很强大很贵,2T内存的一个设备套餐下来2亿人民币

    一是因为内存在寻址和带宽上表现优于磁盘;

    二是数据在磁盘和内存中体积不一样,磁盘中没有”指针”概念,数据不能被引用而复用,数据会冗余”胀出”;所以同样的数据,内存中会生一些空间

  2. 缓存,memcached,redis

    业务发展需要应对高并发和大数据量的场景,但是内存级别的关系型数据库太贵,所以有人想到了折中的方案:把一些高频访问的数据放入内存中,缓存.



redis介绍

架构师必须有的能力:技术对比,技术选型.技术选型对公司很重要.

数据库引擎百科网站:

db-engines

Redis:The world’s fastest In-memory database.

1.5M ops/sec,<1ms latency(with a single AWS EC2 Server)

一般来说,redis秒级10W操作;关系型数据库(MySQL一般是千级)

redis官网:redis.io,中文网站:redis.cn

Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。

Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。



redis比memcached强在哪里?

redis的value有很多类型:

Key

Value

String

字符类型

数值类型

bitmaps

hashes

lists

sets

sorted sets

memcached也是键值对,但是value没有类型的概念,想存储复杂的数据大概需要json格式

json,可以表示很复杂的数据结构,世界上有3种数据表示:

1.单元素 k=a,k=1

2.线性元素 k=[1,2,3],k=[a,x,f]

3.对象k={x=y},复杂的格式k=[{对象},{}]

假设value需要存储一个数组,

  • memcached的弊端在于

    1.存储一个json,那么需要返回value所有的数据到客户端.对server段网卡IO压力较大

    2.客户端要去解码
  • redis的优势,其实类型不是很重要,优势在于

    计算向数据移动


    redis的server中对每种类型都有自己的方法,index(),lpop等,server直接返回所需值,IO压力小,client不必解码计算解析json.



实操



安装redis服务



0. 官网下载,解压:tar xf xxx.tar.gz (不加v规避IO)

这是Redis源码,(基本上Linux中的源码都是C开发的),上来先看README,里面有各种介绍和编译安装步骤

编译:把源码编译成可执行程序;

安装:其实就是一个拷贝的过程

编译安装过程如下,进入源码目录后:



1. make

make是Linux自带的编译工具,可面向多个语言,自动根据同目录下的Makefile编译

Redis用C写的,需要cc命令

如果是新系统(我们大概用的都是centOS),那就 yum install gcc(g是GNU一个开源组织)

安装完gcc后执行make distclean清除上次的编译结果,然后重新执行make

编译后src目录下已经有redis-cli redis-server等可执行程序,这里也可以直接(前台)运行程序



2. make install PREFIX=/data/redis

会把编译后的可执行程序复制一份到指定文件夹,不会和源码混在一起了;但此时还没有成为系统的服务



3. utils/install_server.sh安装服务

先配置redis的环境变量:

vi /etc/profile

在最后面添加两行:

export REDIS_HOME=/data/redis

export PATH=$PATH:$REDIS_HOME/bin

保存后刷新配置文件:source /etc/profile

此时echo $PATH 应该会输出包括Redis/bin的一些路径,说明配置成功了.

这时候就可以在任何地方使用redis-cli等命令运行程序了

进入源码目录的util目录,然后执行./install_server.sh

根据提示一步步配置安装

会配置端口,日志文件路径,数据持久化路径,可执行程序路径

其实是往/etc/init.d/ 路径下复制了一份执行脚本redis_6379,然后加入了chkconfig

我们可以在任意位置service redis_6379 start/status/stop

可以安装多个实例(进程),通过port区分

继续执行./install_server.sh即可,端口,日志,持久化路径不要相同,程序路径可以相同(没必要搞两份程序).

其实也可以复制多份redis.conf,手动改改配置文件,然后执行redis-server redis.conf指定配置文件启动多个redis实例



了解IO NIO

redis是单进程,单线程,单实例的,那为啥会很快呢?

并不是说整个redis服务只有一个线程在工作,而是单线程来处理用户对数据的操作,还有别的线程在做其他事情.

因为redis用了epoll多路复用,所以很快

而redis是单进程的,所以对于同一个客户端的命令的处理是有”顺序”的

nginx的每个worker也是用了epoll,非阻塞多路复用

在这里插入图片描述

Linux系统中,一切皆文件,连接其实就是文件描述符

fd:file descriptor,0标准输入,1标准输出,2错误输出,以及其他的…



早期的BIO(Blocking IO):

socket产生文件描述符

线程/进程通过read命令读文件描述符(fd)

当一个进程去读fd时,如果client数据包还没到,read命令是阻塞的.

一个链接对应一个线程/进程这样就造成很多资源浪费,切换线程成本等

JVM中,一个线程的成本:

堆是共享空间,线程栈是独立的 默认1MB,可调整线程栈的大小

1.线程多了,CPU调度成本增加

2.内存成本,线程栈

在这里插入图片描述



内核发展-同步非阻塞socket nonblock(NIO)

yum install man man-pages 安装一个工具

man可以看8中类型的文档

1类文档:man ls 查看一个Linux命令(e.g.:ls)的详细用法

2类文档: 查看系统调用,内核给用户暴露的方法

man 2 read

可以看看redis进程对应的IO fd:

ps -fe | grep redis 找到redis进程号,假设为6244

ll /proc/6244/fd 会看到各种fd

man 2 socket

可以看到socket可以nonblock

redis一个进程不断的轮询fd

有数据的话就处理,处理完才下一个;没有数据的话就继续轮询.

在这里插入图片描述

这个时期的问题:

轮询发生在用户空间,redis写一个死循环不断轮询

如果有很多fd,那么用户进程就需要轮询调用很多次fd,每次查询fd都要调一次系统调用,内核就会在内核态和用户态切换,



内核发展-select多路复用NIO

man 2 select

allow a program to monitor multiple fds,waiting util one or more fds become “ready” .

多路复用:

先调用select,一次监控很多fd,返回有数据的fd

遍历返回的结果,然后再read有数据的fd

这个时期的问题:

1.传递数据的成本,调select的时候,传参会有数据拷贝(在用户态和内核态间),很多的fd参数会成为累赘

2.粒度不够细,在遍历的时候有可能数据到达了

在这里插入图片描述



内核发展-mmap,epoll

Linux目前还不能实现AIO,只有Windows可以真正实现;Linux可以做到伪AIO,发展过程很复杂

epoll也是属于NIO,而不是AIO

man 2 mmap

用户空间和内核空间相互独立;这里相当于通过mmap搞了块共享空间,把描述符数据存储在共享空间,而不是进程和内核各自维护一份fd;

man epoll

epoll不是一个系统调用,而是一个整体facility,它里面有3个系统调用

大致过程如下:

1.进程先调用epoll的create,返回一个epoll的fd;

epoll通过mmap开辟一块共享空间,增删改由内核完成,查询内核和用户进程都可以

这块共享空间中有一个红黑树和一个链表

2.进程调用epoll的ctl add/delete sfd,把新来的链接放入红黑树中,

2.1进程调用wait(),等待事件(

事件驱动

)

3.当红黑树中的fd有数据到了,就把它放入一个链表中并维护该数据可写还是可读,wait返回;

4.上层用户空间(通过epoll)从链表中取出fd,然后调用read/write读写数据.

所以epoll也是NIO,不是AIO

在这里插入图片描述

mmap和零拷贝不是一回事,啥是零拷贝?

man 2 sendfile

两个文件描述符之间的0拷贝

在这里插入图片描述

sendfile + mmap可以组成kafka:

kafka是运行在JVM上的一个用户进程

在这里插入图片描述



关于epoll和mmap

早期epoll使用mmap共享空间,有个过度,后来丰富了3个api,改变了

这是早期的,后面内核有了响应的sc create ctl wait 就不用自己mmap 了:

The files  fs/pipe.c  and  include/linux/pipe_fs_i.h  has been also modified to extend /dev/epoll to pipes ( pipe() ).
The /dev/epoll implementation resides in two new files driver/char/eventpoll.c and the include/linux/eventpoll.h include file.
The interface of the new /dev/epoll is quite different from the previous one coz it works only by mmapping the device file descriptor while the copy-data-to-user-space has been discarded for efficiency reasons. By avoiding unnecessary copies of data through a common set of shared pages the new /dev/epoll achieves more efficency due 1) less CPU cycles needed to copy the data 2) a lower memory footprint with all the advantages on modern cached memory architectures.
The /dev/epoll implementation uses the new file callback notification machanism to register its callbacks that will store events inside the event buffer. The initialization sequence is :

        if ((kdpfd = open("/dev/epoll", O_RDWR)) == -1) {

        }
        if (ioctl(kdpfd, EP_ALLOC, maxfds))
        {

        }
        if ((map = (char *) mmap(NULL, EP_MAP_SIZE(maxfds), PROT_READ,
                        MAP_PRIVATE, kdpfd, 0)) == (char *) -1)
        {

        }

where  maxfds  is the maximum number of file descriptors that it's supposed to stock inside the polling device. Files are added to the interest set by :

        struct pollfd pfd;

        pfd.fd = fd;
        pfd.events = POLLIN | POLLOUT | POLLERR | POLLHUP;
        pfd.revents = 0;
        if (write(kdpfd, &pfd, sizeof(pfd)) != sizeof(pfd)) {
                ...
        }

and removed with :

        struct pollfd pfd;

        pfd.fd = fd;
        pfd.events = POLLREMOVE;
        pfd.revents = 0;
        if (write(kdpfd, &pfd, sizeof(pfd)) != sizeof(pfd)) {
                ...
        }

The core dispatching code looks like :

        struct pollfd *pfds;
        struct evpoll evp;

        for (;;) {
                evp.ep_timeout = STD_SCHED_TIMEOUT;
                evp.ep_resoff = 0;

                nfds = ioctl(kdpfd, EP_POLL, &evp);
                pfds = (struct pollfd *) (map + evp.ep_resoff);
                for (ii = 0; ii < nfds; ii++, pfds++) {
                        ...
                }
        }

这么一句

 if ((map = (char *) mmap(NULL, EP_MAP_SIZE(maxfds), PROT_READ,
                        MAP_PRIVATE, kdpfd, 0)) == (char *) -1)



版权声明:本文为weixin_42008012原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。