Docker
Abstract
Docker是一个开源的容器引擎,可以将应用软件和应用所需的依赖打包在一起,形成一个标准化单元,类似于Java基于jvm的write once, run anywhere, docker可以实现build once, run anywhere。最近几年,微服务架构火热,而在大型企业中,微服务的运维由于服务的数量比较多而进展困难,容器化服务势在必行。服务容器化使应用程序能够从组件快速组装到发布,消除了开发和QS和IT的摩擦。容器是微服务的基石,没有容器化的微服务是没有灵魂的。docker让容器化操作变得简单,docker的出现让大型的微服务架构得以流行,而微服务的火热又促进了docker发展。
Why
一致的运行环境
开发过程中一个常见的问题是环境一致性问题。由于开发环境、测试环境、生产环境不一致,导致有些 bug 并未在开发过程中被发现。而 Docker 的镜像提供了除内核外完整的运行时环境,确保了应用运行环境一致性,从而不会再出现 「这段代码在我机器上没问题啊」 这类问题。
持续交付和部署
对开发和运维(DevOps)人员来说,最希望的就是一次创建或配置,可以在任意地方正常运行。
使用 Docker 可以通过定制应用镜像来实现持续集成、持续交付、部署。开发人员可以通过 Dockerfile 来进行镜像构建,并结合 持续集成(Continuous Integration) 系统进行集成测试,而运维人员则可以直接在生产环境中快速部署该镜像,甚至结合 持续部署(Continuous Delivery/Deployment) 系统进行自动部署。
而且使用 Dockerfile 使镜像构建透明化,不仅仅开发团队可以理解应用运行环境,也方便运维团队理解应用运行所需条件,帮助更好的生产环境中部署该镜像。
更轻松的迁移
由于 Docker 确保了执行环境的一致性,使得应用的迁移更加容易。Docker 可以在很多平台上运行,无论是物理机、虚拟机、公有云、私有云,甚至是笔记本,其运行结果是一致的。因此用户可以很轻易的将在一个平台上运行的应用,迁移到另一个平台上,而不用担心运行环境的变化导致应用无法正常运行的情况。
更轻松的维护和扩展
Docker 使用的分层存储以及镜像的技术,使得应用重复部分的复用更为容易,也使得应用的维护更新更加简单,基于基础镜像进一步扩展镜像也变得非常简单。此外,Docker 团队同各个开源项目团队一起维护了一大批高质量的 官方镜像,既可以直接在生产环境使用,又可以作为基础进一步定制,大大的降低了应用服务的镜像制作成本。
VM, Vagrant, Docker
最开始,大家就想着直接把整个系统打包再部署,即虚拟机技术,保证了系统和环境的稳定,减少了大量手工操作。但是,缺点也是有的,那就是无法自动化打包过程,于是Vargrant被做了出来,Vargrant使用了vargrantfile文件给开发者提供了定义自动化构建VM镜像的功能。但是,还是有缺点,首先,打包出来的镜像太大,包含了过多不需要使用的依赖;其次,服务启动必须等得VM的系统启动完成后才能启动服务。
为了解决VM的缺点,Docker被开发了出来,它提供了系统上的进程级别的虚拟化技术。与VM需要虚拟化整个硬件和操作系统不同,Docker是直接基于宿主机的操作系统进行虚拟化,性能几乎没有损耗。具体结构差异如下:
虚拟机:
+------------+-------------+
| App A | App B |
+------------+-------------+
| Libs | Libs |
+------------+-------------+
|Guest OS | Guest OS |
+------------+-------------+
| Hypervisor |
+--------------------------+
| HOST OS |
+--------------------------+
| SERVER |
+--------------------------+
Docker:
+------------+-------------+
| App A | App B |
+------------+-------------+
| Libs | Libs |
+------------+-------------+
| Docker Engine |
+--------------------------+
| HOST OS |
+--------------------------+
| SERVER |
+--------------------------+
Docker的组成
docker主要由以下组件组成:
- docker client
- docker daemon
- docker image
- docker container
- docker registry
Docker本身是C/S架构,C端是docker client,S端是docker daemon,docker daemon管理者docker image和docker container,docker index提供镜像索引以及用户认证的功能,docker registry在远程存放镜像,大致架构如下:
+-----------------+
| Docker Daemon |
,---. | -------------- | )------------------(
/ \ | Container1 | | |
/ Docker\ | |--------------- | Docker Registry |
( Client )---------- | Container2 | | |
\ / | | )------------------(
\ / | Container3 | |
`---' | | |
docker pull | ....... | |
docker run | | |
docker build +-----------------+ |
docker ... | |
| |
+----------------+ |
| Docker Index |--------------------------+
+----------------+
Docker Image
docker image是docker容器的基石,保存了docker启动的各种条件,docker container的启动必须基于一个docker image。Docker image位于docker生命周期的构建和打包阶段。
docker使用了union mount的方式构建镜像,采用了ufs的模型。就是docker一次性加载多个文件系统形成一个文件系统层叠的形式,但是在外部看来容器只有一个文件系统可用,docker将这样的文件系统称为docker image。
docker image是一个层层叠加的只读文件系统。最底部是bootfs,即引导文件系统,当容器启动后,引导文件系统将会被卸载。bootfs之上是rootfs,rootfs可以是ubuntu/centos/debian/fedora,与虚拟机启动后由只读状态变为读写状态不同,docker的rootfs永远是只读。
Docker的UFS示例:
+---------------+
| add django |
+---------------+
| add python3 |
+---------------+
|rootfs(ubuntu) |
+---------------+
| bootfs |
+---------------+
Docker Container
Docker Container是Docker的执行单元,通过镜像来启动。Docker container位于docker生命周期的启动和执行阶段。
Docker Container基于镜像启动时,会在镜像构建出的ufs上再添加一个writable layer,一个可写层。当容器刚启动时,该读写层是空的,当容器状态发生改变时,读写层才会有变化。比如当需要修改一个文件时,docker把改文件从下层的只读文件系统中复制到读写层,再进行修改,该文件的只读版本仍然存在,但是已经被读写层上的副本隐藏起来了。这就是Docker中重要的技术Copy on Write(中文翻译为写时复制),每个只读镜像层永远都是只读的,不会变化,当创建出一个新容器时,docker会构建出一个镜像栈,在栈的顶层增加读写层,并与底下的只读层和一些数据就构成了一个容器。容器的分层框架能够使我们快速的构建镜像并发布应用程序。
Docker container创建的镜像栈
+---------------+
| add my app | 读写层(copy on write)
+---------------+
| add django | Read Only
+---------------+
| add python3 | Read Only
+---------------+
|rootfs(ubuntu) | Read Only
+---------------+
| bootfs | Read Only
+---------------+
Docker Registry
Docker仓库分为公有和私有,放着一些已有的镜像,官方的docker registry是Docker Hub。
Namespaces
C++中有namespace这个概念,它实现了代码块的封装,更重要的是代码的隔离,使得不同namespace下可以有相同的变量名但意义不同。在linux里,namespace是一个重要的特性,提供了系统资源的隔离,包括进程/网络/文件系统等。linux实现namespace的重要目的就是为了实现轻量级的虚拟化,也就是容器(linux里的lxc了解一下)。在同一namespace下的进程可以感知彼此的变化,而不能发现其他进程,从而使得一个namespace下的进程认为自身处于一个独立的系统中,便达到了独立和隔离的目的。
Docker目前用到了以下几种namespace进行资源隔离。
- PID, Process ID, 进程隔离;
- NET, Network, 网络隔离;
- IPC, InterProcess Communication, 跨进程通信访问隔离;
- MNT, Mount, 挂载点隔离;
- UTS, Unix Timesharing System, 内核和版本标识隔离;
- USER, 用户和组隔离;
而隔离资源的管理就需要用到Control Groups
Control Groups
Control groups是linux提供的限制/记录/隔离主机物理资源的机制,简称cgroups,为了更好的管理容器,cgroups在2007年并入到linux kernel中。没有cgroups,就没有容器。
cgroups主要负责容器相关的以下内容:
- 资源限制,比如为进程组设置一个内存使用的上限;
- 优先级设定,比如为某些进程组设定更大的cpu和内存资源。
- 资源计量,比如计算进程组使用了多少系统资源。
- 资源控制,比如将进程组挂起和恢复。
正是因为namespaces和cgroups,Docker的容器才拥有了以下能力:
- 文件系统隔离:每个容器都有自己的rootfs;
- 进程隔离: 每个容器都运行在自己的进程环境中;
- 网络隔离: 容器间的虚拟网络接口和IP地址都是分离的;
- 资源隔离和分组: cgroups将CPU和内存等资源独立分配给每个不同的Docker容器。
容器的状态
- 运行
- 已暂停
- 重新启动
- 已退出
Command
$ docker help
$ docker help cp
$ docker run --detach --interactive --tty --name web busybox:latest /bin/bash
$ docker run -d -name wp3 --link wpdb:mysql -p 80 -v /run/lock/apache2 -v /run/apache2 --read-only wordpress:4
$ docker run -d --rm --name wp --env MY_ENVIRONMENT_VAR="test" --read-only wordpress:4
$ docker inspect --format "{{.State.Running}}" wp
$ docker ps
$ docker top web
$ docker run --it
$ docker restart web
$ docker logs web
$ docker exec web ps
$ docker stop xxxx
$ docker search postgres
$ docker save -o myfile.tar busybox:latest
$ docker load -i myfile.tar
$ docker rmi busybox
Dockerfile指令
类似于Makefile, Docker可以根据Dockerfile来构建镜像。Dockerfile是一个文本文件,每一行都是一个Dockerfile指令。接着使用docker build命令就可以构建出一个docker镜像了。Dockerfile是自动化运维的关键。
指令格式
注释
# Comment 这是注释
指令
INSTRUCTION arguments
样例
FROM ubuntu:16:04
LABEL author=fwz version=1.0.1
WORKDIR /root/myapp
COPY myapp .
RUN apt update \
&& apt install -y python3 python3-pip
&& pip3 install -r requirement.txt
EXPOSE 8000
ENTRYPOINT ["/usr/bin/python3", "/root/app/manage.py", "runserver"]
FROM
FROM指令指定了一个基础镜像,接下来的指令都是基于这个基础镜像进行构建。每个Dockerfile都必须包含FROM指令。如果FROM指定的基础镜像不在本地,则会从Dockerhub上拉取。
FROM ubuntu:16:04
FROM python:3.7
RUN
RUN指令是构建时执行的指令,。格式是RUN <command>
或者RUN ['executable', 'param1', 'param2', ...]
,RUN指令默认使用shell执行命令,即/bin/sh -c
执行命令,如果想使用不同的shell,比如bash
,那么可以像这么写:
RUN ['/bin/bash', '-c' 'python3 bootstrap.py']
而且最好RUN指令的内容都写在同一行,防止ufs的层数过多。
CMD
CMD指令是容器启动时执行的指令,可以包含参数,比如:
CMD ["python3", "manage.py", "runserver", "0.0.0.0:8000"]
EXPOSE
用于指定容器监听的内部端口号,不过可以在docker run
的时候更改端口映射;
EXPOSE 27017
COPY
把文件从本地复制到容器中
COPY application.yml /app/
ENTRYPOINT
和CMD类似,也是容器启动时执行的命令,不过可以在容器启动即docker run
时提供参数
ENTRYPOINT ["java", "app.jar"]
LABEL
记录容器相关的变量,docker inspect
时可以看到
LABEL author="菜徐坤" version="7.7.7"
Config Center: Apollo
随着微服务的流行,应用和机器数量急剧增长,程序配置也愈加繁杂:各种功能的开关、参数的配置、服务器的地址等等。同时,我们对程序配置的期望值也越来越高:配置修改后实时生效,灰度发布,分环境、分集群管理,完善的权限、审核机制等等。
一般服务部署到线上之后,当程序发布到生产环境后,一般就是按照预设的逻辑运行,我们无法直接去干预程序的行为,不过可以通过调整配置参数来动态调整程序的行为。
在Spring Cloud的微服务架构方案中虽然提供了Spring Cloud Config来担任配置中心的角色,但是该项目的功能在配置的管理层面还是非常欠缺的。初期我们可以依赖选取的配置存储系统(比如:Gitlab、Github、Gerrit)给我们提供的配置管理界面来操作所有的配置信息,但是这样的管理还是非常粗粒度的。
spring cloud config使用git作分布式配置管理仓库,虽然比较便捷,但是也有一些缺点,比如:修改配置的操作需要修改clone下来的仓库然后再commit和push,缺少通过web界面修改配置的手段;配置需要生效的话,则要求服务重启或者refresh或者git hooks回调等;另外版本管理也是没有比较好的web界面的,只能通过在命令行上使用git命令。授权和审核操作gerrit和gitlab是有提供的,但是粒度比较粗,修改和发布权限没有比较好的分离。在生产环境上,每个服务正在使用什么配置,往往只能靠记忆,很不方便。
spring cloud config既然推荐了git,自然是有优点的,最重要的一点就是足够简单,另外由于缺乏了很多功能,开发人员能开展个性化的开发因而扩展性极强。这就够了。
Ansible
ANSIBLE
[TOC]
场景
为了操作远程主机,我们通常是ssh登录到对应的机子上并输入命令执行,安装软件、修改配置、等等。如果只有单台主机,这么做是完全OK的,顶多是把一些常用操作变成脚本减少重复操作。但是当有了两台主机甚至更多主机之后,同样的命令在另一台主机还要敲一遍,这样的操作就有点枯燥繁琐了。有的人觉得仅仅是多一台主机还好,那如果多10台主机呢,比如需求是在10台机子上配置redis集群,那么ssh登录到每台机子上进行操作是一件很sb的事。
一个简单的解决方案是:使用pssh。pssh可以通过ssh的方式批量发送命令到多台主机并执行,pscp可以批量拷贝文件到多台主机。但是pssh的缺点也很明显,就是功能太少。如果想要通过pssh进行复杂的操作,那就比较困难了。
多主机的配置管理是常见的场景,前人已经造好了轮子,也就是一些自动化运维工具,我们直接使用就行了。比较著名的也是国内用的最多的就是Ansible。
其实本质上Ansible或者说大部分运维工具主要解决以下自动化运维场景:
- 文件传输
- 应用部署
- 配置管理
- 任务流编排
常用自动化运维工具
自动化运维工具主要可以分为两大类:
- 需要装代理的;
- 不需要装代理的;
第一种方式是指我们需要预先在目标主机上安装代理软件(在被控端上放个内鬼,就可以交易了),然后主控端通过通知代理软件来操纵被控端。
第二种方式是不需要在被控端安装代理软件,主要是通过ssh连接到被控主机进行控制操作,主控端需要提前得到被控主机的授权(比如秘钥对)才能进行相应的操作。
当前主流的自动化运维工具有:
- ansible:使用python编写,agentless,适用于中小型应用环境;
- saltstack: 使用python编写,需要在每台机子上安装agent,执行效率更高;
- puppet: 使用ruby编写,功能强大,配置复杂,重型,适合大型环境;
- fabric:使用python编写,agentless,功能比较简单;
由于puppet是使用ruby编写的,国内ruby玩家的数量远少于python,因此saltstack和ansible的使用者相对来说就比较多。fabric由于功能太少,完全能被ansible替代,所以企业用户很少使用fabric做复杂的任务流编排。
Ansible
特性
这里简单说一下ansible的主要特性:
- 模块化:调用特定的模块,完成特定的任务,比如说rabbitmq模块特定用于部署rabbitmq,redis模块特定用户部署redis等;
- 有paramiko,pyyaml,jinja2三个关键模块,分别的功能是ssh连接并操作,解析yaml文件,jinjia2模板引擎;
- 支持自定义模块;
- 基于python实现;
- 部署简单,基于python和ssh,agentless;
- 安全,基于openssh;
- 支持playbook编排任务;
- 幂等性,第一次执行某个ansible的操作和第n次执行同样的ansible操作对主机状态的改变结果是一样的;
- 无需代理不依赖pki;
- 可使用任何语言写模块;
- yaml格式,编排任务,支持丰富的数据结构;
- 较强大的多层解决方案;
安装
只要用pip安装即可。
> pip install ansible
或者
> pip3 install ansible
完成。
配置文件主要在:
/etc/ansible/ansible.cfg
,ansible工具本身的配置。/etc/ansible/hosts
,静态主机清单(Inventory)配置。/etc/ansible/roles/
,ansible-roles可以放在这。
组件
Inventory
主机清单。对于批量主机操作,为了便捷的使用其中的部分主机,可以在inventory file中将其分组命名,默认的inventory file为/etc/ansible/hosts
,inventory file可以有多个,且也可以通过Dynamic Inventory来动态生成。inventory文件的格式符合INI
文件风格,中括号中的字符为组名。可以将同一个主机同时归并到多个不同的组中;如果目标主机使用非默认的SSH端口,还可以在主机名称之后使用冒号加端口号来表明。举个例子:
[webserver]
172.1.1.40:2222
10.2.2.2
www.bbb.com
# 主机机名遵循相似的命名模式,还可使用列表的方式标识个主机
[mysqlserver]
db[01:10].dddd.com
[redisserver]
rds-[a:f].cloud.com
# 在inventory中定义主机时为其添加主机变量以便于在playbook中使用
[appserver]
10.3.3.3 maxMemoryUsage=60
10.4.4.4 servicePort=8099
# 定义指定组内所有主机上的在playbook中可用变量
[appsever:vars]
config-center=172.3.4.5
enable-water=true
AD-Hoc
命令行模式使用ansible。
使用ansible --help
可查看如何使用
fwz@fwz-mbp ~/Workspace> ansible --help
Usage: ansible <host-pattern> [options]
Define and run a single task 'playbook' against a set of hosts
Options:
-a MODULE_ARGS, --args=MODULE_ARGS
module arguments
--ask-vault-pass ask for vault password
-B SECONDS, --background=SECONDS
run asynchronously, failing after X seconds
(default=N/A)
-C, --check don't make any changes; instead, try to predict some
of the changes that may occur
-D, --diff when changing (small) files and templates, show the
differences in those files; works great with --check
......
......
简单的说,语法就是ansible <host-pattern> [-f forks] [-m module_name] [-a args]
本质上,就是调用相应的ansible模块,并传入相应的参数。
举几个例子:
ping所有主机(使用ping模块):
> ansible all -a ping --ask-pass
ansible all -m ping --ask-pass
SSH password:
xx.xx.xx.xxx | SUCCESS => {
"changed": false,
"ping": "pong"
}
改变文件属性(使用file模块):
> ansible mysql-server -m file -a 'owner=mysql group=mysql mode=644 path=/tmp/fstab.ansible'
拷贝文件(使用copy模块):
> ansible storageIndex-server -m copy -a 'src=/home/devops/cloud-relay-1.2.4-SNAPSHOT.tar.gz dest=/root/cloud-storageIndex/cloud-relay-1.2.4.tar.gz'
配置Rabbitmq(使用rabbitmq_user模块):
> ansible rabbitmq-server -m rabbitmq_user -a 'user=new_user password=root tags=administrator vhost=/ state=present'
执行linux的命令(使用shell模块):
> ansible webserver -m shell -a 'netstat -nutlp > netstat_temp.txt'
Playbook
剧本。虽然可以在命令行中使用ansible,但是明显功能有限,无法支持复杂环境的配置管理工作。首先,命令行一次只能执行一条命令,其次,由于通常是手敲命令,因此不好重复。类似于linux中的shell脚本,ansible提供了playbook,翻译为剧本,用于组织基于多个ansible模块交互的工作流编排。
linux的脚本文件是.sh
文件,ansible-playbook则是.yaml
文件,即使用YAML语法。YAML的语法和其他高阶语言类似,并且可以简单表达清单、散列表、标量等数据结构。
直接举个例子:
---
# This playbook will install mysql and create db user and give permissions.
- hosts: $servername
remote_user: root
- name: Install Mysql package
yum: name={{ item }} state=installed
with_items:
- mysql-server
- MySQL-python
- libselinux-python
- libsemanage-python
- name: Configure SELinux to start mysql on any port
seboolean: name=mysql_connect_any state=true persistent=yes
when: sestatus.rc != 0
- name: Create Mysql configuration file
template: src=my.cnf.j2 dest=/etc/my.cnf
notify:
- restart mysql
- name: Start Mysql Service
service: name=mysqld state=started enabled=yes
- name: insert iptables rule
lineinfile: dest=/etc/sysconfig/iptables state=present regexp="{{ mysql_port }}"
insertafter="^:OUTPUT " line="-A INPUT -p tcp --dport {{ mysql_port }} -j ACCEPT"
notify: restart iptables
- name: Create Application Database
mysql_db: name={{ dbname }} state=present
- name: Create Application DB User
mysql_user: name={{ dbuser }} password={{ upassword }} priv=*.*:ALL host='%' state=present
例子很好理解,首先是定义了这个playbook在哪个主机组中执行,以及对应的用户,接着是一组task,分别调用了yum、seboolean、template、service、lineinfile、mysql_db、mysql_user模块。其中使用了notify作handler。
hosts:playbook中的每一个play的目的都是为了让某个或某些主机以某个指定的用户身份执行任务。hosts用于指定要执行指定任务的主机,其可以使一个或多个由冒号分隔主机组;
remote_user: remote_user也可用于各task中,也可以通过指定其通过sudo的方式在远程主机上执行任务,其可用于play全局或其任务;此外,甚至可以在sudo时使用sudo_user指定sudo时切换的用户。
task:play的主题部分是task list。task list中的各任务按次序逐个在hosts中指定的所有主机上执行,即在所有主机上完成第一个任务后再开始第二个。在运行自上而下某playbook时,如果中途发生错误,所有已执行任务都可能回滚,在更正playbook后重新执行一次即可。
taks的目的是使用指定的参数执行模块,而在模块参数中可以使用变量。模块执行是幂等的。这意味着多次执行是安全的,因为其结果均一致。
每个task都应该有其name,用于playbook的执行结果输出,建议其内容尽可能清晰地描述任务执行步骤,如果为提供name,则action的结果将用于输出。
定义task可以使用”action: module options”或”module:options“的格式推荐使用后者以实现向后兼容。如果action一行的内容过多,也中使用在行首使用几个空白字符进行换行。
tasks:
- name:make sure apache is running
service: name=httpd state=started
在众多的模块中,只有command和shell模块仅需要给定一个列表而无需使用”key=value”格式
tasks:
- name: run this command and ignore the result
shell: /usr/bin/somecommand || /bin/true
Handler: 用于当关注的资源发生变化时采取一定的操作。
“notify”这个action可用于在每个play的最后被触发,这样可以避免多次有改变发生时每次都执行执行的操作,取而代之,仅在所有的变化发生完成后一次性地执行指定操作,在notify中列出的操作称为handlers,也即notify中调用handlers中定义的操作。
- name: template configuration file
template: src=template.j2 dest=/etc/foo.conf
notify:
- restart memcached
- restart apache
Roles
ansible自1.2版本引入的新特性,用于层次性、结构化地组织playbook。roles能够根据层次型结构自动转载变量文件、tasks以及handlers等。要使用roles只需要在playbook中使用include指令即可。简单来讲,roles就是通过分别将变量、文件、任务、模板以及处理器放置于单独的目录中,并可以便捷地include他们的一种机制。角色一般用于基于主机构建服务的场景中,但也可以使用于构建守护进程的场景中。
一个常用的role结构:
site.yml
webserver.yml
fooserver.yml
roles/
common/
files/
templates/
tasks/
handlers/
vars/
meta/
webserver/
files/
templates/
tasks/
handlers/
vars/
meta/
在ansible-playbook中可以这么使用role:
- hosts: webserver
roles:
- common
- webserver
向role中传递参数:
- hosts: webserver
roles:
- common
- { role: foo_app_instance, dir:'/opt/a',port:5000}
- { role: foo_app_instance, dir:'/opt/b',port:5001}
- task目录:至少应该包含一个为main.yml的文件,其定义了此角色的任务列表;此文件可以使用include包含其它的位于此目录中的task文件;
- file目录:存放由copy或script等模板块调用的文件;
- template目录:template模块会自动在此目录中寻找jinja2模板文件;
- handlers目录:此目录中应当包含一个main.yml文件,用于定义此角色用到的各handlers,在handler中使用inclnude包含的其它的handlers文件也应该位于此目录中;
- vars目录:应当包含一个main.yml文件,用于定义此角色用到的变量
- meta目录:应当包含一个main.yml文件,用于定义此角色的特殊设定及其依赖关系;ansible1.3及其以后的版本才支持;
- default目录:应当包含一个main.yml文件,用于为当前角色设定默认变量时使用此目录;
官方提供了很多ansible-roles的例子:ansible/ansible-examples
Galaxy
用于分享和下载ansible-role的平台,类似于同性交友网站github。
Points on Serverless
关于Serverless架构
Abstract
最近写的项目其中一个功能点用到了阿里云的函数计算(function computing, 简称fc)。fc属于serverless computing架构模式,是一种新兴的云计算架构,在云计算中也被描述为Faas(Function as a Service),在2014年Amzon首先推出了AWS Lambda,之后各大主流云厂商跟进,在2016年也分别推出了自家的serverless服务,比如阿里的fc和腾讯的scf等。
阿里云函数计算是事件驱动的全托管计算服务。通过函数计算,您无需管理服务器等基础设施,只需编写代码并上传。函数计算会为您准备好计算资源,以弹性、可靠的方式运行您的代码,并提供日志查询、性能监控、报警等功能。借助于函数计算,您可以快速构建任何类型的应用和服务,无需管理和运维。而且,您只需要为代码实际运行所消耗的资源付费,代码未运行则不产生费用。
什么是serverless computing
概念
serverless computing是指开发者在构建和运行应用时无需管理服务器等基础设施。应用被解耦为细粒度的函数,函数是部署和运行的基本单位。用户只为实际使用的资源付费。serverless computing能够帮助应用开发者摆脱服务器等底层基础设施管理的负担,专注于业务层的创新。Serverless不是真的不需要服务器了,而是不用过多的关注服务器。在微服务的架构下,系统被拆分为一系列独立的服务,serverless的粒度更小,更组件化。
serverless函数是基于容器运行的,每个函数实例的运行时环境都由容器提供;每一次函数被调用都会serverless里的调度器都会首先首先查找可用容器,没有可用容器则会创建一个容器并拉取代码来运行函数,这就是冷启动,冷启动比较耗时。
发展
计算的发展演进:
物理机 --> 虚拟机 --> 云计算 --> 容器 --> serverless
云计算:
-------------
| FaaS |
-------------
| Saas |
-------------
| Pass |
-------------
| IaaS |
-------------
从大型物理机到通过虚拟化技术把物理机虚拟成单个的VM资源,从虚拟化集群到把集群搬到云计算上只做简单运维,再到把每一个VM按照运行空间最小化切分成更细的Docker容器,再从Doceker容器变成直接不用管理任何运行环境的Serverless服务。
优势
- 节约使用成本; 虚拟机通常并不能满负载运行,有一些资源浪费。
- 简化设备运维; 无需维护基础设施;
- 实现快速上线; 部署只需要几步操作;
- 降低开发成本; 只需要关注业务代码(java除外)和相关组件;
- 自动扩展能力;
劣势
- 缺乏调试和开发工具;
- 不适合长时间运行应用; serverless函数都有最长运行时间限制;
- 完全依赖云厂商;
- 冷启动时间; serverless函数第一次启动非常耗时;
Reference
Thinking in Function Programming
Thinking in Function Programming
Functor
TODO: explain it
No Side Effect
TODO: explain it
Currying
TODO: explain it
Lazy Evaluation
TODO: explain it
Closure
TODO: explain it
Implements
In Python
Python并不是函数式编程语言, 只是支持一些函数式编程的特性
In Java
Java在8之前不支持函数式编程。Java8新增Stream API支持以一种声明的方式处理数据;
例如:
// java11
var list = List.of("a", "b", "c", "1");
var listUpper = list.parallelStream()
.filter(s -> !s.isEmpty())
.map(s -> s.toUpperCase())
.collect(Collectors.toList());
var sortedList = list.stream()
.sorted()
.collect(Collectors.joining(","));