使用 Ansible 自动部署项目

未分类

如今部署代码真的是一件天大的事。

开发人员努力敲代码,基本没有时间登录服务器并逐个运行部署脚本。但是开发人员知道他们的部署所需要花费的大概时间,对部署各个工作的轻重缓急十分了解。

因此,开发人员与运维人员一起参与部署工作将真正有助于产品发布成功。

未分类

下面我们谈谈用Git的方式更轻松的部署应用。

使用 Git 来自动部署

如果让开发人员使用自己的 Git 仓库工具做部署,这必须在Git配置文件中添加额外的远程Git 仓库地址。

就像这样:

[remote "origin"]
    url = [email protected]:company/project.git
    url = git@remote-server:project.git
    fetch = +refs/heads/*:refs/remotes/origin/*

或者这样

[remote "deploy"]
    url = git@remote-server:project.git      

开发人员只需要在同一时间在推送代码到不同的地方就可以了。如代码示例,一个放在Codebase,另一个用于生产环境部署。

下面是具体操作

我们只需要配置我们的git服务器,使得每当 git push 事件触发时,Git 服务器就远程或本地自动运行部署脚本。

所以,我们需要先配置git 服务器。

首先在 git 服务器上安装 git-core

sudo apt-get install git-core

然后我们需要创建一个用于Git操作用户

sudo useradd git
passwd git

然后添加开发人员的主机的公钥文件,以便开发人员的电脑能够有权限访问 Git 服务器。

cat〜/ .ssh / id_rsa.pub | ssh git @ remote-server“mkdir -p〜/ .ssh && cat >>〜/ .ssh / authorized_keys”

然后在Git服务器上创建一个空的仓库

mkdir -p /home/user/project.git
cd /home/swapnil/project-1.git
git init --bare

这将创建一个git仓库,您可以看到名为 hooks文件夹,这里会存脚本用于接收文件后自动部署项目。

开发人员只需要在最开始的时候将 Git 服务器的项目仓库地址添加到他的项目中即可。

git remote add deploy git @ remote-server:project.git
git push deploy master

这样就直接部署了开发人员的代码。不过这似乎浪费了开发人员不必要的时间,所以我们应该使用 Ansible 来让部署更加轻松。

未分类

首先运行下列命令

ansible-galaxy install PrabhuVignesh.push_to_deploy

然后将开发机器的公钥保存在中即可。

$ cat publickeys.txt

ssh-rsa AAuyFUVFY65tYTFV567YTYTytytYTYTfyuV56FUYTVYTFYTVyTFYUG877686V767R76R67R76R6YTYTfyuVFUYTVYTFYTVyTFYUG877686V767R76R67R76R676V767676V76V768V876V76576V768V876V76Vv76V876VB76v7V76V76V6VBT9LLlNGtLo5pnEXIOPiz9X42ZdxBD721bG5XqDfPnz0JfgAYl6Zw4CXM0F6q0jiAQNJrtrfSNS92x1KMLY8CcVLKOZbpWdUHnouLdKCeM6dBHStpX7yjlb90fRKVZFch87eO0dyAoGWS3oBEYttFYL7s5dm/QV [email protected]

ssh-rsa AAuyFUVFY65tYTFV567YTYTytytYTYTfyuV56FUYTVYTFYTVyTFYUG877686V767R76R67R76R6YTYTfyuVFUYTVYTFYTVyTFYUG877686V767R76R67R76R676V767676V76V768rrtrt54t46Vv76V876VB76v7V76V76V6VBT9LLlNGtLo5pnEXIOPiz9X42ZdxBD721bG5XqDfPnz0JfgAYl6Zb6sevtrrtyyw4CXM0F6q0jiAQNJrtrfSNS92x1KMLY8CcVLKOZbpWdUHnouLdKCeM6dBHStpX7yjlb90fRKVZFch87eO0dyAoGWS3oBEYttFYL7s5dm/QV [email protected]

ssh-rsa AAuyFUVFY65tYTFV567YTYTytytYTYTfyuV56FUYTVYTFYTVyTFYUG877686V767R76R67R76R6YTYTfyuVFUYTVYTFYTVyTFYUG877686V767R76R67R76R63435343r4r3453434r34r34f34356trfhgjhgkjmnhi87t665rg6dvrdxdcser536c5456yvu675678br56er56v5v7y5y5gbr5576un8n78i8it0F6q0jiAQNJrtrfSNS92x1KMLY8CcVLKOZbpWdUHnouLdKCeM6dBHStpX7yjlb90fRKVZFch87eO0dyAoGWS3oBEYttFYL7s5dm/QV [email protected]

接着在环境变量中添加 path_for_authorized_keys,git_repository_path,post_receive_script,pre_receive_script 中的路径。

---
  roles:
    - role: PrabhuVignesh.push_to_deploy
      path_for_authorized_keys: /path/to/public_key/file
      git_repository_path: /home/path/to/your/repo.git
      post_receive_script: "script to deploy the code"
      pre_receive_script: "Prepare storing code"

git服务器的所有配置将通过 Ansible 来完成。

当开发人员将代码推送到 git 服务器时,应用程序就会执行用于后接收和预接收运行的脚本,如“puppet deploy script”,“cap deploy …”等,并且将自动部署。

另外使用这样的方式,只有在推送Master分支的时候才会运行部署脚本,这就避免不必要的部署。

#!/bin/bash
do
    if [[ $ref =~ .*/master$ ]];
    then
        echo "I am master branch push and i will run deployment script"
        # Run deployment script.....
    else
        echo "Ref $ref successfully received.  Doing nothing: only the master branch may be deployed on this server."
    fi
done

结论

GIT真的是一个令人赞叹的IT自动化工具,特别让开发和运维更好的协作,让发布新版本更加轻松。

Ansible 批量创建用户 密码注意事项

user模块是请求的是useradd, userdel, usermod三个指令,goup模块请求的是groupadd, groupdel, groupmod 三个指令。

user模块

home:指定用户的家目录,需要与createhome配合使用

groups:指定用户的属组

uid:指定用的uid

password:指定用户的密码

name:指定用户名

createhome:是否创建家目录 yes|no

system:是否为系统用户

remove:当state=absent时,remove=yes则表示连同家目录一起删除,等价于userdel -r

state:是创建还是删除(present,absent) 

shell:指定用户的shell环境

generate_ssh_key:是否为相关用户生成SSH密钥。 这不会覆盖现有的SSH密钥。 

ssh_key_bits:可选择指定要创建的SSH密钥中的位数。

ssh_key_passphrase:设置SSH密钥的密码。 如果没有提供密码,SSH密钥将默认没有密码。 

ssh_key_file:指定SSH密钥文件名(可选)。 如果这是一个相对的文件名,那么它将是相对于用户的主目录。 

ssh_key_type:指定要生成的SSH密钥的类型(可选)。 可用的SSH密钥类型将取决于目标主机上的实现。

使用ansibile创建用户有两种方法

方法1:使用user 模块,更简单,命令如下:

[root@ansible ~]# ansible ansible_group -m user -a 'name=test password=-18SyrVeFt/xU uid=1000 shell=/bin/bash home=/home/admin_group/test group=admin_group state=present' --sudo

验证创建的用户:

[root@ansible ~]# ansible ansible_group -m raw -a 'tail /etc/passwd|grep test'

online-web-asset-3 | SUCCESS | rc=0 >>
test:x:1000:501::/home/admin_group/test:/bin/bash
Shared connection to 192.168.5.1 closed.

注意:passwd的值不能是明文,passwd关键字后面应该是密文,且密文将被保存在/etc/shadow文件中,密文的生成命令为:

[root@ansible ~]# openssl passwd -salt -1 "you passwd"

然后将生成的密文,如: 填写到ansible ansible_group -m user -a ‘name=username password= ’ 中的passwd关键字后面即可。

方法2:使用playbook

创建playbook文件 useradd.yml,内容如下:

[root@ansible ansible]# more useradd.yml 

---
- hosts: "{{hosts}}"
  gather_facts: false
  user: centos
  sudo: yes
  vars:
#    user: test123
  tasks:
    - name: add user
      user: name={{user}} password={{passwd}} home=/home/admin_group/{{user}} group=admin_group shell=/bin/bash
      state: present
  tags:
    - user

保存后执行如下命令检测:

[root@ansible ~]# ansible-playbook --syntax-check useradd.yml -e "hosts=qsh_test user=test1 passwd=-11Fe1z.bZ5o."

然后运行playbook,使用 -e选项传入参数

[root@ansible ~]# ansible-playbook useradd.yml -e "hosts=qsh_test user=test1 passwd=-11Fe1z.bZ5o."

gitlab 403 forbidden 并发引起ip被封问题解决方法

步骤:

  • 打开/etc/gitlab/gitlab.rb文件。
  • 查找gitlab_rails[‘rack_attack_git_basic_auth’]关键词。
  • 取消注释
  • 修改ip_whitelist白名单属性,加入Gitlab部署的IP地址。
gitlab_rails[‘rack_attack_git_basic_auth’] = {
‘enabled’ => true,
‘ip_whitelist’ => [“127.0.0.1″,”Gitlab部署的IP地址”],
‘maxretry’ => 300,
‘findtime’ => 5,
‘bantime’ => 60
}
  • 配置好后,执行gitlab-ctl reconfigure即可。

修复群晖 GitLab 升级失败的问题

如果你最近使用群晖套件中心的 GitLab( 9.4.4-0050 ) ,并且勾选转换数据库( MariaDB => postgresql ),那么你很可能遇到丢失数据的问题( Version: 10.6.4-0051 ).表现为打开 GitLab 首页提示设置 root 密码,登录后台显示所有项目丢失.

故障的原因是群晖”借鉴”的 MySQL to PostgreSQL Converter 项目存在一些问题 .群晖内的具体路径为/volume1/@appstore/Docker-GitLab/mysql-postgresql-converter/db_converter.py,转换时会发生UnicodeDecodeError: ‘utf8’ codec can’t decode byte 0x9c错误.截至目前尚有 30 个 PR 未处理.群晖科技在自家稳定版产品中未确认方案普适性,原项目首页强调 use with care 被无视,置客户数据安全于不顾,最终导致悲剧发生.后续我会专门谈一谈为什么 synology NAS 是危险的产品,选购群晖是对数据的不负责行为.本篇只说怎么找回你宝贵的数据.

修改 docker 环境变量,使用原 MariaDB 数据库

1.使用 admin 组的账户登录群晖 SSH .警告: 如果你不知道这步怎么操作,本修复指南可能对你来说较复杂,极易出现数据丢失.请向技术人员求助.

2.如果之前没有执行过备份( bundle exec rake gitlab:backup:create RAILS_ENV=production ),那么执行下备份.路径类似/volume1/docker/gitlab/.

3.为 gitlab_user 授权.如果不熟悉 SQL 可以在套件中心中安装 phpMyAdmin ,然后在权限中增加用户账户的 host name 为 172.17.0.% .

4.测试数据库权限:

docker exec -it synology_gitlab bash  # 进入容器终端
mysql -u gitlab_user -p -h 172.17.0.1 -P 3307  # 测试数据库链接情况
exit  # 退出容器

5.套件中心 => 已安装 => GitLab => 停止.

6.docker => synology_gitlab => 编辑 => 环境变量

DB_PORT 3307
DB_TYPE mysql
DB_HOST 172.17.0.1

7.套件中心 => 已安装 => GitLab => 启动.

修复数据库故障

GitLab 官方明确表示不推荐使用 MariaDB / MySQL 数据库. 群晖由于某种原因仍然在使用 MariaDB 10 .本部分重点解决各类数据库问题.

1.停止 GitLab 服务service gitlab stop.
2.登录 MariaDB 或直接使用 phpMyAdmin .让数据库支持长索引.

use gitlab;
SET storage_engine=INNODB;
SET GLOBAL innodb_file_per_table=1, innodb_file_format=Barracuda, innodb_large_prefix=1;
SET GLOBAL log_bin_trust_function_creators = 1;

3.转换原有数据使用 utf8mb4 编码.注意下面的命令每一条的回显都要作为 SQL 语句再执行.

SELECT CONCAT('ALTER TABLE `', TABLE_NAME,'` ROW_FORMAT=DYNAMIC;') AS 'Copy & run these SQL statements:' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA="gitlab" AND TABLE_TYPE="BASE TABLE" AND ROW_FORMAT!="Dynamic";

回显类似

ALTER TABLE `abuse_reports` ROW_FORMAT=DYNAMIC;
ALTER TABLE `appearances` ROW_FORMAT=DYNAMIC;
ALTER TABLE `application_settings` ROW_FORMAT=DYNAMIC;
ALTER TABLE `audit_events` ROW_FORMAT=DYNAMIC;
ALTER TABLE `award_emoji` ROW_FORMAT=DYNAMIC;
ALTER TABLE `boards` ROW_FORMAT=DYNAMIC;
ALTER TABLE `broadcast_messages` ROW_FORMAT=DYNAMIC;
ALTER TABLE `chat_names` ROW_FORMAT=DYNAMIC;
ALTER TABLE `chat_teams` ROW_FORMAT=DYNAMIC;
ALTER TABLE `ci_build_trace_section_names` ROW_FORMAT=DYNAMIC;
ALTER TABLE `ci_build_trace_sections` ROW_FORMAT=DYNAMIC;
ALTER TABLE `ci_builds` ROW_FORMAT=DYNAMIC;
ALTER TABLE `ci_group_variables` ROW_FORMAT=DYNAMIC;
ALTER TABLE `ci_job_artifacts` ROW_FORMAT=DYNAMIC;
ALTER TABLE `ci_pipeline_schedule_variables` ROW_FORMAT=DYNAMIC;
ALTER TABLE `ci_pipeline_schedules` ROW_FORMAT=DYNAMIC;
ALTER TABLE `ci_pipeline_variables` ROW_FORMAT=DYNAMIC;
ALTER TABLE `ci_pipelines` ROW_FORMAT=DYNAMIC;
ALTER TABLE `ci_runner_projects` ROW_FORMAT=DYNAMIC;
ALTER TABLE `ci_runners` ROW_FORMAT=DYNAMIC;
ALTER TABLE `ci_stages` ROW_FORMAT=DYNAMIC;
ALTER TABLE `ci_trigger_requests` ROW_FORMAT=DYNAMIC;
ALTER TABLE `ci_triggers` ROW_FORMAT=DYNAMIC;
ALTER TABLE `ci_variables` ROW_FORMAT=DYNAMIC;
ALTER TABLE `cluster_platforms_kubernetes` ROW_FORMAT=DYNAMIC;
ALTER TABLE `cluster_projects` ROW_FORMAT=DYNAMIC;
ALTER TABLE `cluster_providers_gcp` ROW_FORMAT=DYNAMIC;
ALTER TABLE `clusters` ROW_FORMAT=DYNAMIC;
ALTER TABLE `clusters_applications_helm` ROW_FORMAT=DYNAMIC;
ALTER TABLE `container_repositories` ROW_FORMAT=DYNAMIC;
ALTER TABLE `conversational_development_index_metrics` ROW_FORMAT=DYNAMIC;
ALTER TABLE `deploy_keys_projects` ROW_FORMAT=DYNAMIC;
ALTER TABLE `deployments` ROW_FORMAT=DYNAMIC;
ALTER TABLE `emails` ROW_FORMAT=DYNAMIC;
ALTER TABLE `environments` ROW_FORMAT=DYNAMIC;
ALTER TABLE `events` ROW_FORMAT=DYNAMIC;
ALTER TABLE `feature_gates` ROW_FORMAT=DYNAMIC;
ALTER TABLE `features` ROW_FORMAT=DYNAMIC;
ALTER TABLE `fork_network_members` ROW_FORMAT=DYNAMIC;
ALTER TABLE `fork_networks` ROW_FORMAT=DYNAMIC;
ALTER TABLE `forked_project_links` ROW_FORMAT=DYNAMIC;
ALTER TABLE `gcp_clusters` ROW_FORMAT=DYNAMIC;
ALTER TABLE `gpg_key_subkeys` ROW_FORMAT=DYNAMIC;
ALTER TABLE `gpg_keys` ROW_FORMAT=DYNAMIC;
ALTER TABLE `gpg_signatures` ROW_FORMAT=DYNAMIC;
ALTER TABLE `group_custom_attributes` ROW_FORMAT=DYNAMIC;
ALTER TABLE `identities` ROW_FORMAT=DYNAMIC;
ALTER TABLE `issue_assignees` ROW_FORMAT=DYNAMIC;
ALTER TABLE `issue_metrics` ROW_FORMAT=DYNAMIC;
ALTER TABLE `issues` ROW_FORMAT=DYNAMIC;
ALTER TABLE `keys` ROW_FORMAT=DYNAMIC;
ALTER TABLE `label_links` ROW_FORMAT=DYNAMIC;
ALTER TABLE `label_priorities` ROW_FORMAT=DYNAMIC;
ALTER TABLE `labels` ROW_FORMAT=DYNAMIC;
ALTER TABLE `lfs_objects` ROW_FORMAT=DYNAMIC;
ALTER TABLE `lfs_objects_projects` ROW_FORMAT=DYNAMIC;
ALTER TABLE `lists` ROW_FORMAT=DYNAMIC;
ALTER TABLE `members` ROW_FORMAT=DYNAMIC;
ALTER TABLE `merge_request_diff_commits` ROW_FORMAT=DYNAMIC;
ALTER TABLE `merge_request_diff_files` ROW_FORMAT=DYNAMIC;
ALTER TABLE `merge_request_diffs` ROW_FORMAT=DYNAMIC;
ALTER TABLE `merge_request_metrics` ROW_FORMAT=DYNAMIC;
ALTER TABLE `merge_requests` ROW_FORMAT=DYNAMIC;
ALTER TABLE `merge_requests_closing_issues` ROW_FORMAT=DYNAMIC;
ALTER TABLE `milestones` ROW_FORMAT=DYNAMIC;
ALTER TABLE `namespaces` ROW_FORMAT=DYNAMIC;
ALTER TABLE `notes` ROW_FORMAT=DYNAMIC;
ALTER TABLE `notification_settings` ROW_FORMAT=DYNAMIC;
ALTER TABLE `oauth_access_grants` ROW_FORMAT=DYNAMIC;
ALTER TABLE `oauth_access_tokens` ROW_FORMAT=DYNAMIC;
ALTER TABLE `oauth_applications` ROW_FORMAT=DYNAMIC;
ALTER TABLE `oauth_openid_requests` ROW_FORMAT=DYNAMIC;
ALTER TABLE `pages_domains` ROW_FORMAT=DYNAMIC;
ALTER TABLE `personal_access_tokens` ROW_FORMAT=DYNAMIC;
ALTER TABLE `project_authorizations` ROW_FORMAT=DYNAMIC;
ALTER TABLE `project_auto_devops` ROW_FORMAT=DYNAMIC;
ALTER TABLE `project_custom_attributes` ROW_FORMAT=DYNAMIC;
ALTER TABLE `project_features` ROW_FORMAT=DYNAMIC;
ALTER TABLE `project_group_links` ROW_FORMAT=DYNAMIC;
ALTER TABLE `project_import_data` ROW_FORMAT=DYNAMIC;
ALTER TABLE `project_statistics` ROW_FORMAT=DYNAMIC;
ALTER TABLE `projects` ROW_FORMAT=DYNAMIC;
ALTER TABLE `protected_branch_merge_access_levels` ROW_FORMAT=DYNAMIC;
ALTER TABLE `protected_branch_push_access_levels` ROW_FORMAT=DYNAMIC;
ALTER TABLE `protected_branches` ROW_FORMAT=DYNAMIC;
ALTER TABLE `protected_tag_create_access_levels` ROW_FORMAT=DYNAMIC;
ALTER TABLE `protected_tags` ROW_FORMAT=DYNAMIC;
ALTER TABLE `push_event_payloads` ROW_FORMAT=DYNAMIC;
ALTER TABLE `redirect_routes` ROW_FORMAT=DYNAMIC;
ALTER TABLE `releases` ROW_FORMAT=DYNAMIC;
ALTER TABLE `routes` ROW_FORMAT=DYNAMIC;
ALTER TABLE `schema_migrations` ROW_FORMAT=DYNAMIC;
ALTER TABLE `sent_notifications` ROW_FORMAT=DYNAMIC;
ALTER TABLE `services` ROW_FORMAT=DYNAMIC;
ALTER TABLE `snippets` ROW_FORMAT=DYNAMIC;
ALTER TABLE `spam_logs` ROW_FORMAT=DYNAMIC;
ALTER TABLE `subscriptions` ROW_FORMAT=DYNAMIC;
ALTER TABLE `system_note_metadata` ROW_FORMAT=DYNAMIC;
ALTER TABLE `taggings` ROW_FORMAT=DYNAMIC;
ALTER TABLE `tags` ROW_FORMAT=DYNAMIC;
ALTER TABLE `timelogs` ROW_FORMAT=DYNAMIC;
ALTER TABLE `todos` ROW_FORMAT=DYNAMIC;
ALTER TABLE `trending_projects` ROW_FORMAT=DYNAMIC;
ALTER TABLE `u2f_registrations` ROW_FORMAT=DYNAMIC;
ALTER TABLE `uploads` ROW_FORMAT=DYNAMIC;
ALTER TABLE `user_agent_details` ROW_FORMAT=DYNAMIC;
ALTER TABLE `user_custom_attributes` ROW_FORMAT=DYNAMIC;
ALTER TABLE `user_synced_attributes_metadata` ROW_FORMAT=DYNAMIC;
ALTER TABLE `users` ROW_FORMAT=DYNAMIC;
ALTER TABLE `users_star_projects` ROW_FORMAT=DYNAMIC;
ALTER TABLE `web_hook_logs` ROW_FORMAT=DYNAMIC;
ALTER TABLE `web_hooks` ROW_FORMAT=DYNAMIC;');

4.转换原有数据使用新的表空间( tablespace )格式.注意下面的命令每一条的回显都要作为 SQL 语句再执行.

SET foreign_key_checks = 0;
SELECT CONCAT('ALTER TABLE `', TABLE_NAME,'` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;') AS 'Copy & run these SQL statements:' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA="gitlab" AND TABLE_COLLATION != "utf8mb4_general_ci" AND TABLE_TYPE="BASE TABLE";

回显类似

ALTER TABLE `abuse_reports` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `appearances` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `application_settings` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `audit_events` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `award_emoji` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `boards` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `broadcast_messages` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `chat_names` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `chat_teams` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `ci_build_trace_section_names` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `ci_build_trace_sections` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `ci_builds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `ci_group_variables` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `ci_job_artifacts` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `ci_pipeline_schedule_variables` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `ci_pipeline_schedules` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `ci_pipeline_variables` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `ci_pipelines` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `ci_runner_projects` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `ci_runners` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `ci_stages` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `ci_trigger_requests` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `ci_triggers` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `ci_variables` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `cluster_platforms_kubernetes` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `cluster_projects` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `cluster_providers_gcp` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `clusters` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `clusters_applications_helm` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `container_repositories` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `conversational_development_index_metrics` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `deploy_keys_projects` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `deployments` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `emails` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `environments` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `events` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `feature_gates` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `features` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `fork_network_members` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `fork_networks` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `forked_project_links` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `gcp_clusters` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `gpg_key_subkeys` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `gpg_keys` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `gpg_signatures` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `group_custom_attributes` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `identities` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `issue_assignees` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `issue_metrics` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `issues` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `keys` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `label_links` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `label_priorities` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `labels` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `lfs_objects` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `lfs_objects_projects` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `lists` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `members` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `merge_request_diff_commits` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `merge_request_diff_files` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `merge_request_diffs` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `merge_request_metrics` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `merge_requests` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `merge_requests_closing_issues` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `milestones` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `namespaces` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `notes` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `notification_settings` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `oauth_access_grants` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `oauth_access_tokens` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `oauth_applications` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `oauth_openid_requests` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `pages_domains` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `personal_access_tokens` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `project_authorizations` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `project_auto_devops` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `project_custom_attributes` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `project_features` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `project_group_links` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `project_import_data` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `project_statistics` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `projects` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `protected_branch_merge_access_levels` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `protected_branch_push_access_levels` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `protected_branches` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `protected_tag_create_access_levels` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `protected_tags` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `push_event_payloads` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `redirect_routes` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `releases` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `routes` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `schema_migrations` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `sent_notifications` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `services` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `snippets` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `spam_logs` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `subscriptions` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `system_note_metadata` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `taggings` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `tags` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `timelogs` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `todos` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `trending_projects` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `u2f_registrations` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `uploads` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `user_agent_details` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `user_custom_attributes` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `user_synced_attributes_metadata` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `users` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `users_star_projects` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `web_hook_logs` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE `web_hooks` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
SET foreign_key_checks = 1;

5.执行数据库限制任务.

bundle exec rake add_limits_mysql RAILS_ENV=production

6.修改数据库迁移任务.由于 GitLab 官方要求不允许跨多个小版本升级,所以我们会遇到一些奇怪的错误,这里给出一些修复方案,我只遇到了一个.

Mysql2::Error: Index column size too large. The maximum column size is 767 bytes.: CREATE UNIQUE INDEX `index_lfs_file_locks_on_project_id_and_path`  ON `lfs_file_locks` (`project_id`, `path`)

解决方案:

use gitlab;
DROP TABLE lfs_file_locks;
# db/migrate/20180116193854_create_lfs_file_locks.rb
# create_table :lfs_file_locks do |t| 修改为
create_table :lfs_file_locks, options: 'ROW_FORMAT=DYNAMIC'  do |t|

7.执行数据库迁移.

bundle exec rake db:migrate

预编译资源

bundle exec rake gettext:compile RAILS_ENV=production
bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production
bundle exec rake cache:clear RAILS_ENV=production

备份与恢复

bundle exec rake gitlab:backup:create RAILS_ENV=production  # 你会找到一个类似 /home/git/data/backups/1531046569_2018_07_14_10.6.4_gitlab_backup.tar 的文件
bundle exec rake gitlab:backup:restore RAILS_ENV=production

cnpm+gitlab-ci 模块自动发布方案

为什么会探讨这个主题

在我司现有web前端项目构建过程中,一直使用gulp + rjs的方案进行项目打包,由于整站项目过于庞大,即使是以相当好的机器多核打包,也需要4分钟+的时间,遂产生了将模块全部使用npm管理,并拆分子项目,达到每个通用模块都可自动构建,单元测试,保证质量并稳步迭代的目的。

项目其余细节

公司内部内容,略过

cnpm与cnpmjs.org的部署

cnpm原理

为npm原有的一些命令设置了代理,让你可以方便的连接到自己的私有服务,并同时使用npm上的资源包,您可以选择使用命令直接同步npm官方包(实质是向您的cnpmjs.org私有服务发送命令,并同步线上资源,存储下来),也可以直接通过cnpm接洽的原生npm直接下载,取决于您的需求

部署

cnpm官方通过开源项目cnpmjs.org整合了cnpm.registry和cnpm.web两个服务,让用户可以通过cnpm指定registry使用私有服务。

这里只介绍使用sqlite3作为存储私有包的记录数据库,即官方wiki中的5分钟部署cnpmjs。

Deploy-a-private-npm-registry-in-5-minutes

直接根据以上链接阐述内容,您可以立刻配置起一个cnpmjs.org服务.

配置cnpmjs.org

值得注意的是,您如果没有配置云存储服务器,将会默认使用fs-cnpm的配置,默认将publish的包存放在~/.cnpmjs.org/downloads下

默认的sqlite数据库位于~/.cnpmjs.org/data.sqlite

cnpmjs.org相关配置位于cnpmjs.org的包的config/index.js中

注意:bindingHost默认为127.0.0.1,请置空字符串或已注册的具名地址或内网可访问的ip地址,以便在内网其他机器上访问该服务

如果需使用其他云存储配置,已有数据库,请按照官方wiki修改相应配置。

# 后台启动cnpmjs.org服务,如有需要,可使用其他守护进程
nohup cnpmjs.org start --admins='myname,othername' 
  --scopes='@my-company-name,@other-name' &

当安装好cnpmjs.org并配置好后,需要使用命令写入或者直接配置(至少运行一次)私有仓库作用域(scopes)以及私有仓库管理员名称。不明白的可以去npm官方文档了解一下

cnpmjs.org的registry服务和web服务默认位于7001,7002两个端口,7002即是cnpmjs.org的web网页,可以查看发布的相关私有包或同步的包

配置cnpm

cnpm set registry http://bindingHost:7001

将registry服务地址设置为你启动的cnpmjs.org registry服务地址

第一次登录

也就是注册。

原因是调用cnpm login的时候,走到cnpmjs.org服务,会通过以下路由

app.put('/-/user/org.couchdb.user::name', addUser);

输入下面内容开始创建

cnpm login myname

测试私有npm是否畅通

新建一个项目,cnpm init之

name设置为'@my-company-name/test'

cnpm publish尝试,上传成功

数据备份

相应包如果由于宕机或其他原因消失了是一件很麻烦的事情,请定时去默认数据存储文件夹备份一份(如果使用了其他已有的数据库,云存储,请自行备份),以便恢复。

gitlab的部署

这里使用的是ce-11.0.3版本,按照官方傻瓜式部署,直接可以部署上

请区分ce和ee两个不同版本

EXTERNAL_URL配置与上步骤配置bindingHost类似

其余邮件配置,其他数据库配置(如不用默认数据库),以及其他配置,请自行搜索或寻找官方文档。

gitlab首次启动

使用10 11等版本,首次启动的默认管理员用户密码不再如同以前一样是root 5iveL!fe了,

在新版本中,gitlab-rails web服务于auth中间件中判断了用户是否为第一次登陆,如果为第一次登陆,将会跳转到修改密码页,只需简单进入布置好的网页服务设置root密码即可。

gitlab首次进入修改不了密码?

。。。虽然我是个低级前端工程师,并且我chrome打开了f12,但是我没看到报错,查看了各种权限是否分配错误,查看了是不是编译出错,查看了web服务的auth中间件以及 changepassword服务,查看了数据库,各种可能都找了一遍,就差没直接改数据库,没找到原因。。。

然后我看到了这篇文章修改GitLab Web服务默认端口和初次登录密码报错问题 https://ykzm.in/archives/1010

尝试换了个浏览器,好了。。。没有继续深究为什么

安装gitlab-runner

请直接参考官方文档 在任意一台机器上无脑安装即可

配置gitlab-runner

也请直接参考官方文档

或参考我的渣文档最底部的资料链接

重头戏

gitlab-ci cnpm配合做到一键发布。

gitlab-ci cnpm配合前情提要

首先,用户需要保证runner机器上也装了cnpm,至于怎么装就随便了。

cnpm登录后并不会直接颁布给你一个_authtoken,所以想直接通过在gitlab中的ci配置里写入_authtoken私密变量并进行私有包的发布是不现实的。

cnpm的源码中将publish命令直接拿给原生npm执行了,自己只是对参数进行了一些包装,最终执行 npm publish argv命令

cnpm可以做到的就是修改配置路径(–userconfig参数配置项),读取用户cnpmrc配置。

在设置了–userconfig路径以后,cnpm会将一系列参数直接传递给npm , npm通过 core包中的Conf模块会将你的config文件解析掉,生成一个token并执行publish的put操作。

基于这点我们就可以做到测试完毕并且自动发布私有包了

具体实施步骤

如果您没有修改过指定的.cnpmrc客户配置,当您登陆过cnpm后,会在用户目录下,也就是~/下发现.cnpmrc配置文件。

他大概是这样的

registry=http://yourbindingRegistryServerHost
//yourbindingHost:always-auth=false
//yourbindingRegistryServiceHost:_password=加密过的密码串
//yourbindingRegistryServiceHost:username=myname
//yourbindingRegistryServiceHost:[email protected]

那么思路就很明白了,我们可以这样写我们的发布脚本脚本

// publish.js
const fs = require('fs');
const argv = process.argv;
const un = argv[2];
const pw = argv[3];
const em = argv[4];
const r = argv[5];
const exec = require('child_process').exec;

if (un && pw && em && r) {
    const or = r.replace(/^https?:/, '')
    const result = `
registry=${r}
${or}:always-auth=false
${or}:_password=${pw}
${or}:username=${un}
${or}:email=${em}
`;
    fs.writeFileSync('./.cnpmrc', result);
    exec('cnpm publish --userconfig=' + process.cwd() + '/.cnpmrc', (error, stdout, stderr) => {
        console.log(`stdout: ${stdout}`);
        console.log(`stderr: ${stderr}`);
        if (error) {
            console.error(`exec error: ${error}`);
            process.exit(1);
        }
    })
}

这样写我们的ci配置

before_script:
  - npm i
stages:
  - test
  - publish
job1:
  stage: test
  script:
    - npm run test
  only:
    - master
  tags:
    - qie
job2:
  stage: publish
  script:
    - 'node ./publish.js $CNPM_UN $CNPM_PW $CNPM_EM $CNPM_R'

$CNPM_UN $CNPM_PW $CNPM_EM $CNPM_R为在gitlab仓库中设置的CI私密变量

效果

未分类

未分类

未分类

Gitlab 8.x runner安装与配置

介绍

  Gitlab 8.x之后默认集成了Gitlab CI,意味着支持了持续集成相关功能。每一次集成操作都需要对应的runner来跑代码构建、测试、发布等操作。Runner实际上就是为Gitlab的持续集成指定一个环境。

安装

官方文档地址:https://docs.gitlab.com/runner/install/
  Gitlab Runner的版本需要跟Gitlab对应,这里有一个对照表。最新的版本对照表中并没有Gitlab8.X对应的Runner版本,查了一下Gitlab8.X对应的Runner版本为1.X,所以这里选择runner 1.11.2版本。

  这里运行Gitlab与Runner的环境均为CentOS,之前尝试在windows上安装runner,对接Linux上的Gitlab,发现在Gitlab runner运行的控制台出现乱码问题。

0. 准备

在opt下创建gitlab-runner目录并进入该目录,后续执行的操作与所有的资源都放在这个目录中

cd /opt
mkdir gitlab-runner
cd gitlab-runner/

1. 下载

下载安装资源到gitlab-runner目录中

sudo wget https://gitlab-ci-multi-runner-downloads.s3.amazonaws.com/v1.11.2/binaries/gitlab-ci-multi-runner-linux-386

2. 添加运行权限

sudo chmod +x gitlab-ci-multi-runner-linux-386

3. 创建用户

sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash

4. 安装

./gitlab-ci-multi-runner-linux-386 install --user=gitlab-runner --working-directory=/opt/gitlab-runner
sudo gitlab-ci-multi-runner-linux-386 start

配置

  经过上面的步骤,Runner就已经跑起来了,剩下的还需要Runner与项目对接起来。Runner的类型分为Shared, specific and group Runners。这里选择specific类型,即单独的项目使用。

  在Gitlab项目的setting-runner中,配置过程中会使用到url和token如下所示:

未分类

1.运行register命令

./gitlab-ci-multi-runner-linux-386 register

之后就按照提示就行了

2.输入url地址
3.输入token
4.输入描述,任意即可
5.输入标签,这里直接Enter跳过
6.选择Runner executor,这里选择shell

到这里就已经注册成功了,输入./gitlab-ci-multi-runner-linux-386 list就能看到上面的注册的条目。

官方文档地址:https://docs.gitlab.com/runner/register/index.html

其它

  上面两个步骤做完后,此时按理说Gitlab就能调用Runner跑持续集成了,实际当中还会碰到其它问题,整理如下。

权限问题

  如果在Gitlab的Build控制台上报无法创建文件夹、无法运行bash等,证明创建的GitLab Runner权限不够。
此时,我这里是修改GitLab Runner的权限跟root保持一致。

vim /etc/passwd

通过上面命令可以编辑用户对应的权限,我这里打开默认为gitlab-runner:x:601:601:GitLab Runner:/home/gitlab-runner:/bin/bash,权限组修改为跟root的一致gitlab-runner:x:0:0:GitLab Runner:/home/gitlab-runner:/bin/bash。(root的权限组名为0)

这里在另外一台机器上还碰到这样修改了也不好使的问题,最终gitlab-runner install的时候,直接指定为root,而不新创建用户。

环境问题

由于Runner运行需要环境支撑,比如git、node、npm等,需要在Runner所在的服务器上准备好所有的依赖。

Linux Node安装

# 下载
wget https://nodejs.org/dist/v8.11.3/node-v8.11.3-linux-x64.tar.xz
# 解压
tar -xf  node-v8.11.3-linux-x64.tar.xz
# 建立软链接,实现全局访问
ln -s /opt/gitlab-runner/node-v8.11.3-linux-x64/bin/node /usr/local/bin/node
ln -s /opt/gitlab-runner/node-v8.11.3-linux-x64/bin/npm /usr/local/bin/npm

此时,输入node -v就能看到node的版本了。

使用软连接方式可能对非root用户无效,可以转而使用配置环境变量的方式

# 修改配置文件
vim /etc/profile
#set for nodejs,新增NODE_HOME并放到PATH上
export JAVA_HOME=/opt/soft/java
export NODE_HOME=/opt/gitlab-runner/node-v8.11.3-linux-x64  
export CLASSPATH=$JAVA_HOME/lib:$JAVA_HOME/jre/lib
export PATH=$PATH:$JAVA_HOME/bin:$NODE_HOME/bin

在vim环境下点击i进入插入状态,编辑完成后按Esc键,然后输入 :wq 按回车保存退出。
备注:内外环境还需修改NPM的镜像源,比如修改为npm config set registry https://registry-npm.daojia-inc.com/

附录 部分GitLab-Runner常用命令

1.gitlab-runner帮助:gitlab-runner –help

2.gitlab-runner指定命令帮助:gitlab-runner –help

3.注册runner:gitlab-runner register

4.注销runner:gitlab-runner unregister

5.当前运行的runner:gitlab-runner list

6.启动runner:gitlab-runner start

7.停止runner:gitlab-runner stop

8.重启runner:gitlab-runner restart

9.查询runner状态:gitlab-runner status

docker compose实战部署django

第一步,因为应用将要运行在一个满足所有环境依赖的 Docker 容器里面,那么我们可以通过编辑 Dockerfile 文件来指定 Docker 容器要安装内容。内容如下:

FROM python:3

ENV PYTHONUNBUFFERED 1

RUN mkdir /code

WORKDIR /code

ADD requirements.txt /code/

RUN pip install -r requirements.txt

ADD . /code/

第二步,在 requirements.txt 文件里面写明需要安装的具体依赖包名。

Django>=1.8,<2.0

psycopg2

第三步, docker-compose.yml 文件将把所有的东西关联起来。它描述了应用的构成(一个web 服务和一个数据库)、使用的 Docker 镜像、镜像之间的连接、挂载到容器的卷,以及服务开放的端口。

version: "3"

services:

    db:

        image: postgres

    web:

        build: .

        command: python3 manage.py runserver 0.0.0.0:8000

        volumes:

            – .:/code

        ports:

            – "8000:8000"

        links:

            – db

现在我们就可以使用 docker-compose run 命令启动一个 Django 应用了。

$ docker-compose run web django-admin.py startproject django_example .

Compose 会先使用 Dockerfile 为 web 服务创建一个镜像,接着使用这个镜像在容器里运行django-admin.py startproject composeexample 指令。这将在当前目录生成一个 Django 应用。

$ ls

Dockerfile docker-compose.yml django_example manage.py requ

irements.txt

如果你的系统是 Linux,记得更改文件权限。

sudo chown -R $USER:$USER .

首先,我们要为应用设置好数据库的连接信息。用以下内容替换 django_example/settings.py文件中 DATABASES = … 定义的节点内容。

DATABASES = {

    'default': {

        'ENGINE': 'django.db.backends.postgresql',

         'NAME': 'postgres',

         'USER': 'postgres',

         'HOST': 'db',

         'PORT': 5432,

    }

}

这些信息是在 postgres 镜像固定设置好的。然后,运行 docker-compose up :

$ docker-compose up

django_db_1 is up-to-date

Creating django_web_1 …

Creating django_web_1 … done

Attaching to django_db_1, django_web_1

db_1 | The files belonging to this database system will be owned by user "postgres".

db_1 | This user must also own the server process.

db_1 |

db_1 | The database cluster will be initialized with locale "en_US.utf8".

db_1 | The default database encoding has accordingly been set to "UTF8".

db_1 | The default text search configuration will be set to "english".

web_1 | Performing system checks…

web_1 |

web_1 | System check identified no issues (0 silenced).

web_1 |

web_1 | November 23, 2017 – 06:21:19

web_1 | Django version 1.11.7, using settings 'django_example.settings'

web_1 | Starting development server at http://0.0.0.0:8000/

web_1 | Quit the server with CONTROL-C.

这个 Django 应用已经开始在你的 Docker 守护进程里监听着 8000 端口了。打开127.0.0.1:8000 即可看到 Django 欢迎页面。

你还可以在 Docker 上运行其它的管理命令,例如对于同步数据库结构这种事,在运行完docker-compose up 后,在另外一个终端进入文件夹运行以下命令即可:

$ docker-compose run web python manage.py syncdb

docker-compose up 时提示挂载目录open permission denied

docker-compose up 时提示挂载目录open permission denied

[root@localhost nginx-php]# docker-compose up
Creating php-fpm ... 
Creating php-fpm ... done
Creating nginx ... 
Creating nginx ... done
Attaching to php-fpm, nginx
php-fpm    | [15-Dec-2017 06:53:36] NOTICE: fpm is running, pid 1
php-fpm    | [15-Dec-2017 06:53:36] NOTICE: ready to handle connections
nginx      | nginx: [alert] could not open error log file: open() "/var/log/nginx/error.log" failed (13: Permission denied)
nginx      | 2017/12/15 06:53:36 [emerg] 1#1: open() "/var/log/nginx/error.log" failed (13: Permission denied)
nginx exited with code 1
^CGracefully stopping... (press Ctrl+C again to force)
Stopping php-fpm ... done
[root@localhost nginx-php]#

原因是SELinux搞的鬼,禁用就好了

#查看SELinux状态(如果SELinux status参数为enabled即为开启状态)
/usr/sbin/sestatus -v 

#临时关闭
setenforce 0

#修改配置文件重启机器禁用(将SELINUX=enforcing改为SELINUX=disabled)
vim /etc/selinux/config

基于Redis实现分布式锁

背景

在很多互联网产品应用中,有些场景需要加锁处理,比如:秒杀,全局递增ID,楼层生成等等。大部分的解决方案是基于DB实现的,Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系。其次Redis提供一些命令SETNX,GETSET,可以方便实现分布式锁机制。

Redis命令介绍

使用Redis实现分布式锁,有两个重要函数需要介绍

SETNX命令(SET if Not eXists)
语法:
SETNX key value
功能:
当且仅当 key 不存在,将 key 的值设为 value ,并返回1;若给定的 key 已经存在,则 SETNX 不做任何动作,并返回0。

GETSET命令
语法:
GETSET key value
功能:
将给定 key 的值设为 value ,并返回 key 的旧值 (old value),当 key 存在但不是字符串类型时,返回一个错误,当key不存在时,返回nil。

GET命令
语法:
GET key
功能:
返回 key 所关联的字符串值,如果 key 不存在那么返回特殊值 nil 。

DEL命令
语法:
DEL key [KEY …]
功能:
删除给定的一个或多个 key ,不存在的 key 会被忽略。

兵贵精,不在多。分布式锁,我们就依靠这四个命令。但在具体实现,还有很多细节,需要仔细斟酌,因为在分布式并发多进程中,任何一点出现差错,都会导致死锁,hold住所有进程。

加锁实现

SETNX 可以直接加锁操作,比如说对某个关键词foo加锁,客户端可以尝试
SETNX foo.lock <current unix time>

如果返回1,表示客户端已经获取锁,可以往下操作,操作完成后,通过
DEL foo.lock

命令来释放锁。
如果返回0,说明foo已经被其他客户端上锁,如果锁是非堵塞的,可以选择返回调用。如果是堵塞调用调用,就需要进入以下个重试循环,直至成功获得锁或者重试超时。理想是美好的,现实是残酷的。仅仅使用SETNX加锁带有竞争条件的,在某些特定的情况会造成死锁错误。

处理死锁

在上面的处理方式中,如果获取锁的客户端端执行时间过长,进程被kill掉,或者因为其他异常崩溃,导致无法释放锁,就会造成死锁。所以,需要对加锁要做时效性检测。因此,我们在加锁时,把当前时间戳作为value存入此锁中,通过当前时间戳和Redis中的时间戳进行对比,如果超过一定差值,认为锁已经时效,防止锁无限期的锁下去,但是,在大并发情况,如果同时检测锁失效,并简单粗暴的删除死锁,再通过SETNX上锁,可能会导致竞争条件的产生,即多个客户端同时获取锁。

C1获取锁,并崩溃。C2和C3调用SETNX上锁返回0后,获得foo.lock的时间戳,通过比对时间戳,发现锁超时。
C2 向foo.lock发送DEL命令。
C2 向foo.lock发送SETNX获取锁。
C3 向foo.lock发送DEL命令,此时C3发送DEL时,其实DEL掉的是C2的锁。
C3 向foo.lock发送SETNX获取锁。

此时C2和C3都获取了锁,产生竞争条件,如果在更高并发的情况,可能会有更多客户端获取锁。所以,DEL锁的操作,不能直接使用在锁超时的情况下,幸好我们有GETSET方法,假设我们现在有另外一个客户端C4,看看如何使用GETSET方式,避免这种情况产生。

C1获取锁,并崩溃。C2和C3调用SETNX上锁返回0后,调用GET命令获得foo.lock的时间戳T1,通过比对时间戳,发现锁超时。
C4 向foo.lock发送GESET命令,
GETSET foo.lock
并得到foo.lock中老的时间戳T2

如果T1=T2,说明C4获得时间戳。
如果T1!=T2,说明C4之前有另外一个客户端C5通过调用GETSET方式获取了时间戳,C4未获得锁。只能sleep下,进入下次循环中。

现在唯一的问题是,C4设置foo.lock的新时间戳,是否会对锁产生影响。其实我们可以看到C4和C5执行的时间差值极小,并且写入foo.lock中的都是有效时间错,所以对锁并没有影响。
为了让这个锁更加强壮,获取锁的客户端,应该在调用关键业务时,再次调用GET方法获取T1,和写入的T0时间戳进行对比,以免锁因其他情况被执行DEL意外解开而不知。以上步骤和情况,很容易从其他参考资料中看到。客户端处理和失败的情况非常复杂,不仅仅是崩溃这么简单,还可能是客户端因为某些操作被阻塞了相当长时间,紧接着 DEL 命令被尝试执行(但这时锁却在另外的客户端手上)。也可能因为处理不当,导致死锁。还有可能因为sleep设置不合理,导致Redis在大并发下被压垮。最为常见的问题还有

GET返回nil时应该走那种逻辑?

第一种走超时逻辑
C1客户端获取锁,并且处理完后,DEL掉锁,在DEL锁之前。C2通过SETNX向foo.lock设置时间戳T0 发现有客户端获取锁,进入GET操作。
C2 向foo.lock发送GET命令,获取返回值T1(nil)。
C2 通过T0>T1+expire对比,进入GETSET流程。
C2 调用GETSET向foo.lock发送T0时间戳,返回foo.lock的原值T2
C2 如果T2=T1相等,获得锁,如果T2!=T1,未获得锁。

第二种情况走循环走setnx逻辑
C1客户端获取锁,并且处理完后,DEL掉锁,在DEL锁之前。C2通过SETNX向foo.lock设置时间戳T0 发现有客户端获取锁,进入GET操作。
C2 向foo.lock发送GET命令,获取返回值T1(nil)。
C2 循环,进入下一次SETNX逻辑

两种逻辑貌似都是OK,但是从逻辑处理上来说,第一种情况存在问题。当GET返回nil表示,锁是被删除的,而不是超时,应该走SETNX逻辑加锁。走第一种情况的问题是,正常的加锁逻辑应该走SETNX,而现在当锁被解除后,走的是GETST,如果判断条件不当,就会引起死锁,很悲催,我在做的时候就碰到了,具体怎么碰到的看下面的问题

GETSET返回nil时应该怎么处理?

C1和C2客户端调用GET接口,C1返回T1,此时C3网络情况更好,快速进入获取锁,并执行DEL删除锁,C2返回T2(nil),C1和C2都进入超时处理逻辑。
C1 向foo.lock发送GETSET命令,获取返回值T11(nil)。
C1 比对C1和C11发现两者不同,处理逻辑认为未获取锁。
C2 向foo.lock发送GETSET命令,获取返回值T22(C1写入的时间戳)。
C2 比对C2和C22发现两者不同,处理逻辑认为未获取锁。

此时C1和C2都认为未获取锁,其实C1是已经获取锁了,但是他的处理逻辑没有考虑GETSET返回nil的情况,只是单纯的用GET和GETSET值就行对比,至于为什么会出现这种情况?一种是多客户端时,每个客户端连接Redis的后,发出的命令并不是连续的,导致从单客户端看到的好像连续的命令,到Redis server后,这两条命令之间可能已经插入大量的其他客户端发出的命令,比如DEL,SETNX等。第二种情况,多客户端之间时间不同步,或者不是严格意义的同步。

时间戳的问题

我们看到foo.lock的value值为时间戳,所以要在多客户端情况下,保证锁有效,一定要同步各服务器的时间,如果各服务器间,时间有差异。时间不一致的客户端,在判断锁超时,就会出现偏差,从而产生竞争条件。
锁的超时与否,严格依赖时间戳,时间戳本身也是有精度限制,假如我们的时间精度为秒,从加锁到执行操作再到解锁,一般操作肯定都能在一秒内完成。这样的话,我们上面的CASE,就很容易出现。所以,最好把时间精度提升到毫秒级。这样的话,可以保证毫秒级别的锁是安全的。

分布式锁的问题

1:必要的超时机制:获取锁的客户端一旦崩溃,一定要有过期机制,否则其他客户端都降无法获取锁,造成死锁问题。
2:分布式锁,多客户端的时间戳不能保证严格意义的一致性,所以在某些特定因素下,有可能存在锁串的情况。要适度的机制,可以承受小概率的事件产生。
3:只对关键处理节点加锁,良好的习惯是,把相关的资源准备好,比如连接数据库后,调用加锁机制获取锁,直接进行操作,然后释放,尽量减少持有锁的时间。
4:在持有锁期间要不要CHECK锁,如果需要严格依赖锁的状态,最好在关键步骤中做锁的CHECK检查机制,但是根据我们的测试发现,在大并发时,每一次CHECK锁操作,都要消耗掉几个毫秒,而我们的整个持锁处理逻辑才不到10毫秒,玩客没有选择做锁的检查。
5:sleep学问,为了减少对Redis的压力,获取锁尝试时,循环之间一定要做sleep操作。但是sleep时间是多少是门学问。需要根据自己的Redis的QPS,加上持锁处理时间等进行合理计算。
6:至于为什么不使用Redis的muti,expire,watch等机制,可以查一参考资料,找下原因。

锁测试数据

未使用sleep

第一种,锁重试时未做sleep。单次请求,加锁,执行,解锁时间

未分类

可以看到加锁和解锁时间都很快,当我们使用

ab -n1000 -c100 'http://sandbox6.wanke.etao.com/test/test_sequence.php?tbpm=t'
AB 并发100累计1000次请求,对这个方法进行压测时。

未分类

我们会发现,获取锁的时间变成,同时持有锁后,执行时间也变成,而delete锁的时间,将近10ms时间,为什么会这样?
1:持有锁后,我们的执行逻辑中包含了再次调用Redis操作,在大并发情况下,Redis执行明显变慢。
2:锁的删除时间变长,从之前的0.2ms,变成9.8ms,性能下降近50倍。
在这种情况下,我们压测的QPS为49,最终发现QPS和压测总量有关,当我们并发100总共100次请求时,QPS得到110多。当我们使用sleep时

使用Sleep时

单次执行请求时

未分类

我们看到,和不使用sleep机制时,性能相当。当时用相同的压测条件进行压缩时

未分类

获取锁的时间明显变长,而锁的释放时间明显变短,仅是不采用sleep机制的一半。当然执行时间变成就是因为,我们在执行过程中,重新创建数据库连接,导致时间变长的。同时我们可以对比下Redis的命令执行压力情况

未分类

上图中细高部分是为未采用sleep机制的时的压测图,矮胖部分为采用sleep机制的压测图,通上图看到压力减少50%左右,当然,sleep这种方式还有个缺点QPS下降明显,在我们的压测条件下,仅为35,并且有部分请求出现超时情况。不过综合各种情况后,我们还是决定采用sleep机制,主要是为了防止在大并发情况下把Redis压垮,很不行,我们之前碰到过,所以肯定会采用sleep机制。

Redis查漏补缺:最易错过的技术要点大扫盲

考虑到绝大部分写业务的程序员在实际开发中使用Redis时,只会Setvalue和Getvalue两个操作,对Redis整体缺乏一个认知。又恰逢笔者有同事下周要去培训Redis,所以笔者斗胆以Redis为主题,对Redis常见问题做一个总结,希望能够扫除大家的知识盲点。

本文围绕以下几点进行阐述:

  • 为什么使用Redis
  • 使用Redis有什么缺点
  • 单线程的Redis为什么这么快
  • Redis的数据类型,以及每种数据类型的使用场景
  • Redis的过期策略以及内存淘汰机制
  • Redis和数据库双写一致性问题
  • 如何应对缓存穿透和缓存雪崩问题
  • 如何解决Redis的并发竞争问题

一、为什么使用Redis

笔者认为,在项目中使用Redis,主要是从两个角度去考虑:性能和并发。当然,Redis还具备可做分布式锁等功能的其它功能,但如果只是为了分布式锁这些其它功能,完全还有其它中间件(如Zookpeer等)可以代替,并不是非要使用Redis。

因此,这个问题主要从性能和并发两个角度去答:

1、性能

如下图所示,我们在碰到需要执行耗时特别久、且结果不频繁变动的SQL时,就特别适合将运行结果放入缓存。这样,后面的请求就去缓存中读取,使得请求能够迅速响应。

未分类

题外话:忽然想聊一下这个迅速响应的标准——其实根据交互效果的不同,这个响应时间没有固定标准。不过曾经有人这么告诉我:“在理想状态下,我们的页面跳转需要在瞬间解决,对于页内操作则需要在刹那间解决。另外,超过一弹指的耗时操作要有进度提示,并且可以随时中止或取消,这样才能给用户最好的体验。”

那么瞬间、刹那、一弹指具体是多少时间呢?

根据《摩诃僧祗律》记载:一刹那者为一念,二十念为一瞬,二十瞬为一弹指,二十弹指为一罗预,二十罗预为一须臾,一日一夜有三十须臾。

那么,经过周密的计算,一瞬间为0.36秒,一刹那有0.018秒,一弹指长达7.2秒。

2、并发

如下图所示,在大并发的情况下,所有的请求直接访问数据库,数据库会出现连接异常。这个时候,就需要使用Redis做一个缓冲操作,让请求先访问到Redis,而不是直接访问数据库。

未分类

二、使用Redis有什么缺点

大家用Redis这么久,这个问题是必须要了解的,基本上使用Redis都会碰到一些问题,常见的主要是四方面的问题:

  • 缓存和数据库双写一致性问题
  • 缓存雪崩问题
  • 缓存击穿问题
  • 缓存的并发竞争问题

这四个问题,笔者个人觉得在项目中比较常遇见,具体解决方案,后文会给出。

三、单线程的Redis为什么这么快

这个问题其实是对Redis内部机制的一个考察。其实根据笔者的面试经验,很多人其实都不知道Redis是单线程工作模型。所以,这个问题还是应该要复习一下的。主要是以下三点:

  • 纯内存操作
  • 单线程操作,避免了频繁的上下文切换
  • 采用了非阻塞I/O多路复用机制

我们现在仔细地说一说I/O多路复用机制,因为这个说法实在是太通俗了,通俗到一般人都不懂是什么意思。打一个比方:小曲在S城开了一家快递店,负责同城快送服务。小曲因为资金限制,雇佣了一批快递员,然后小曲发现资金不够了,只够买一辆车送快递。

经营方式一:
客户每送来一份快递,小曲就让一个快递员盯着,然后快递员开车去送快递。慢慢的小曲就发现了这种经营方式存在很多问题,几十个快递员基本上时间都花在了抢车上了,大部分快递员都处在闲置状态,谁抢到了车,谁就能去送快递。

随着快递的增多,快递员也越来越多,小曲发现快递店里越来越挤,没办法雇佣新的快递员了,快递员之间的协调很花时间,大部分时间花在抢车上。综合上述缺点,小曲痛定思痛,提出了下面的经营方式↓

经营方式二:
小曲只雇佣一个快递员,客户送来的快递,小曲按送达地点标注好,然后依次放在一个地方。最后,那个快递员依次去取快递,一次拿一个,开着车去送快递,送好了就回来拿下一个快递。

上述两种经营方式对比,是不是明显觉得第二种,效率更高、更好呢?在上述比喻中:

  • 每个快递员→每个线程
  • 每个快递→每个Socket(I/O流)
  • 快递的送达地点→Socket的不同状态
  • 客户送快递请求→来自客户端的请求
  • 小曲的经营方式→服务端运行的代码
  • 一辆车→CPU的核数

于是我们有如下结论:

  • 经营方式一就是传统的并发模型,每个I/O流(快递)都有一个新的线程(快递员)管理。
  • 经营方式二就是I/O多路复用。只有单个线程(一个快递员),通过跟踪每个I/O流的状态(每个快递的送达地点),来管理多个I/O流。

下面类比到真实的Redis线程模型,如图所示:

未分类

参照上图,简单来说就是,我们的Redis-client在操作的时候,会产生具有不同事件类型的Socket。在服务端,有一段I/O多路复用程序,将其置入队列之中。然后文件事件分派器依次去队列中取,转发到不同的事件处理器中。

需要说明的是,这个I/O多路复用机制,Redis还提供了Select、Epoll、Evport、Kqueue等多路复用函数库,大家可以自行去了解。

四、Redis的数据类型及各自使用场景

看到这个问题,是不是觉得它很基础?其实笔者也这么觉得。然而根据面试经验发现,至少80%的人答不上这个问题。建议在项目中用到后,再类比记忆,体会更深,不要硬记。基本上,一个合格的程序员五种类型都会用到:

1、String

这个其实没什么好说的,最常规的Set/Get操作,Value可以是String也可以是数字,一般做一些复杂的计数功能的缓存。

2、Hash

这里Value存放的是结构化的对象,比较方便的就是操作其中的某个字段。笔者在做单点登录的时候,就是用这种数据结构存储用户信息,以CookieId作为Key,设置30分钟为缓存过期时间,能很好地模拟出类似Session的效果。

3、List

使用List的数据结构,可以做简单的消息队列的功能。另外还有一个就是,可以利用Lrange命令,做基于Redis的分页功能,性能极佳,用户体验好。

4、Set

因为Set堆放的是一堆不重复值的集合,所以可以做全局去重的功能。

为什么不用JVM自带的Set进行去重?因为我们的系统一般都是集群部署,使用JVM自带的Set比较麻烦,难道为了做一个全局去重,再起一个公共服务?太麻烦了。

另外,就是利用交集、并集、差集等操作,可以计算共同喜好、全部的喜好、自己独有的喜好等功能。

5、Sorted Set

Sorted Set多了一个权重参数Score,集合中的元素能够按Score进行排列。可以做排行榜应用,取TOP N操作。另外,Sorted Set还可以用来做延时任务。最后一个应用就是可以做范围查找。

五、Redis的过期策略及内存淘汰机制

这个问题其实相当重要,从这个问题就可以看出来到底Redis有没有用到位。比如,你Redis只能存5G数据,可是你写了10G,那会删5G的数据。怎么删的?这个问题思考过么?还有,你的数据已经设置了过期时间,但是时间到了,内存占用率还是比较高,有思考过原因么?

Redis采用的是定期删除+惰性删除策略。

为什么不用定时删除策略?

定时删除,用一个定时器来负责监视Key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除Key,因此没有采用这一策略。

定期删除+惰性删除是如何工作的呢?

定期删除,Redis默认每个100ms检查是否有过期的Key,有过期Key则删除。需要说明的是,Redis不是每个100ms将所有的Key检查一次,而是随机抽取进行检查(如果每隔100ms,全部Key进行检查,Redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多Key到时间没有删除。

于是,惰性删除派上用场。也就是说在你获取某个Key的时候,Redis会检查一下,这个Key如果设置了过期时间,那么是否过期了?如果过期了此时就会删除。

采用定期删除+惰性删除就没其他问题了么?

不是的,如果定期删除没删除Key。然后你也没及时去请求Key,也就是说惰性删除也没生效。这样,Redis的内存会越来越高,那么就应该采用内存淘汰机制。

在Redis.conf中有一行配置:

# maxmemory-policy volatile-lru

该配置就是配内存淘汰策略的:

  • Noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。应该没人使用吧;
  • Allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的Key。推荐使用,目前项目在用这种;
  • Allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key,应该也没人使用吧;
  • Volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的Key。这种情况一般是把Redis既当缓存又做持久化存储的时候才用。不推荐;
  • Volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个Key。依然不推荐;
  • Volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的Key优先移除。不推荐。

PS:如果没有设置Expire的Key,不满足先决条件(Prerequisites);那么Volatile-lru、Volatile-random和Volatile-ttl策略的行为,和Noeviction(不删除)基本上一致。

六、Redis和数据库双写一致性问题

一致性问题是分布式常见问题,还可以再分为最终一致性和强一致性。数据库和缓存双写,就必然会存在不一致的问题,想要回答这个问题,就要先明白一个前提:如果对数据有强一致性要求,就不能放缓存。我们所做的一切,只能保证最终一致性。

另外,我们所做的方案其实从根本上来说,只能说降低不一致发生的概率,无法完全避免。因此,有强一致性要求的数据不能放缓存。

《分布式数据库与缓存双写一致性方案解疑》 https://mp.weixin.qq.com/s?__biz=MzI4NTA1MDEwNg==&mid=2650767895&idx=1&sn=eb87586d2b7748021fd8cd1791d5d39e&chksm=f3f93782c48ebe94ec0e85bc9e08ac1b478614ca500128a7f42b6dee60c20bc4ade86ce044be&scene=21#wechat_redirect 给出了详细的分析,在这里简单地说一说:首先,采取正确更新策略,先更新数据库,再删缓存;其次,因为可能存在删除缓存失败的问题,提供一个补偿措施即可,例如利用消息队列。

七、应对缓存穿透和缓存雪崩问题

关于“如何应对缓存穿透和缓存雪崩”这两个问题,说句实在话,一般中小型传统软件企业很难碰到。如果有大并发的项目,流量有几百万左右,这两个问题一定要深刻考虑:

1、应对缓存穿透

缓存穿透,即黑客故意去请求缓存中不存在的数据,导致所有的请求都怼到数据库上,从而数据库连接异常。

解决方案:

  • 利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库,没得到锁,则休眠一段时间重试;
  • 采用异步更新策略,无论Key是否取到值,都直接返回。Value值中维护一个缓存失效时间,缓存如果过期,异步起一个线程去读数据库,更新缓存,需要做缓存预热(项目启动前,先加载缓存)操作;
  • 提供一个能迅速判断请求是否有效的拦截机制,比如利用布隆过滤器,内部维护一系列合法有效的Key,迅速判断出,请求所携带的Key是否合法有效,如果不合法,则直接返回。

2、应对缓存雪崩

缓存雪崩,即缓存同一时间大面积的失效,这个时候又来了一波请求,结果请求都怼到数据库上,从而导致数据库连接异常。

解决方案:

  • 给缓存的失效时间加上一个随机值,避免集体失效;
  • 使用互斥锁,但是该方案吞吐量明显下降了;
  • 双缓存。我们有两个缓存,缓存A和缓存B。缓存A的失效时间为20分钟,缓存B不设失效时间,自己做缓存预热操作。然后细分以下几个小点:
    a. 从缓存A读数据库,有则直接返回;
    b. A 没有数据,直接从B读数据,直接返回,并且异步启动一个更新线程;
    c. 更新线程同时更新缓存A和缓存B。

八、如何解决Redis并发竞争Key问题

这个问题大致就是同时有多个子系统去Set一个Key。这个时候要注意什么呢?本人提前百度了一下,发现大家思考的答案基本都是推荐用Redis事务机制。但本人不推荐使用Redis的事务机制。因为我们的生产环境,基本都是Redis集群环境,做了数据分片操作。你一个事务中有涉及到多个Key操作的时候,这多个Key不一定都存储在同一个Redis-Server上。因此,Redis的事务机制,十分鸡肋。

解决方法如下:

如果对这个Key操作不要求顺序

这种情况下,准备一个分布式锁,大家去抢锁,抢到锁就做Set操作即可,比较简单。

如果对这个Key操作要求顺序

假设有一个Key1,系统A需要将Key1设置为ValueA,系统B需要将Key1设置为ValueB,系统C需要将Key1设置为ValueC。期望按照Key1的Value值按照 ValueA→ValueB→ValueC的顺序变化。这种时候我们在数据写入数据库的时候,需要保存一个时间戳。假设时间戳如下:

  • 系统A Key 1 {ValueA 3:00}
  • 系统B Key 1 {ValueB 3:05}
  • 系统C Key 1 {ValueC 3:10}

那么,假设这会系统B先抢到锁,将Key1设置为{ValueB 3:05}。接下来系统A抢到锁,发现自己的ValueA的时间戳早于缓存中的时间戳,那就不做Set操作了。以此类推。

其他方法,比如利用队列,将Set方法变成串行访问也可以。总之,灵活变通。

九、总结

本文对Redis的常见问题做了一个总结。大部分是笔者自己在工作中遇到,以及以前面试别人的时候常问的一些问题,希望大家能够有所收获。