一次Mysql改表引发LVS下RS机器全下线的问题

某天下午,正在和code苦战的我突然收到报警短信,告警我们有个业务电信机房LVS下的RS机器全部下线了。第一时间去看机器负载情况,发现CPU IDLE在80%左右,其他各项指标也都正常;怀疑是LVS的KeepAlive程序出问题了,上管理平台点了一遍RS上线,发现服务恢复了,于是未做进一步排查,只向OPS同学反馈了一下。

然而,刚过了半个小时,同样的报警又来了,看来还得找到根本原因。挑了一台机器保留现场,并在管理平台将其他机器操作上线,以保证充分的排查时间。

先检查Nginx allweb.log中lvscheck相关的日志,发现状态码全部为499且request_time达到5s:

[tabalt@server01 ~]$ tail -100 /data/nginx/logs/allweb.log | grep lvscheck
10.18.42.2 92 0 5.000[s] - - [12/Jul/2017:18:29:18 +0800] "GET /status.php HTTP/1.0" 499 - "-" "KeepAliveClient" lvscheck.domain.com 10.20.12.60 - -
10.18.42.2 92 0 5.000[s] - - [12/Jul/2017:18:29:22 +0800] "GET /status.php HTTP/1.0" 499 - "-" "KeepAliveClient" lvscheck.domain.com 10.20.12.60 - -
10.18.42.2 92 0 5.000[s] - - [12/Jul/2017:18:29:24 +0800] "GET /status.php HTTP/1.0" 499 - "-" "KeepAliveClient" lvscheck.domain.com 10.20.12.60 - -
...

原来KeepAlive程序请求http://lvscheck.domain.com/status.php页面探测服务情况时,竟然过了5s都没有收到响应,于是主动断开请求并将RS下线了。但机器很闲,为什么/status.php会处理超过5s呢?

检查PHP-FPM的日志,发现有报错/data/www/front/index.php文件执行很慢:

[tabalt@server01 ~]$ tail /data/php/log/php-fpm.log
12-Jul-2017 18:29:18] WARNING: [pool www] child 3988, script '/data/www/front/index.php' (request: "GET /index.php") executing too slow (11.301960 sec), logging
[12-Jul-2017 18:29:22] WARNING: [pool www] child 3945, script '/data/www/front/index.php' (request: "GET /index.php") executing too slow (11.863325 sec), logging
[12-Jul-2017 18:29:24] WARNING: [pool www] child 3887, script '/data/www/front/index.php' (request: "GET /index.php") executing too slow (10.498795 sec), logging
...

但/data/www/front/index.php只是入口文件,从这个日志看不出来问题在哪里,再检查下PHP-FPM的慢日志:

[tabalt@server01 ~]$ tail -100 /data/php/log/www.log.slow
...
script_filename = /data/www/front/index.php
[0x00007fecbd613f90] execute() /data/www/vendor/andals/vine/src/Component/Mysql/Driver.php:218
[0x00007fecbd613ec0] doExecute() /data/www/vendor/andals/vine/src/Component/Mysql/Driver.php:66
[0x00007fecbd613df0] query() /data/www/vendor/andals/vine/src/Component/Mysql/Dao/Base.php:206
[0x00007fecbd613d80] simpleQuery() /data/www/src/app/Model/Dao/Base.php:65
[0x00007fecbd613cc0] selectByParamsForFront() /data/www/src/app/Model/Svc/SqlBase.php:211
[0x00007fecbd613c10] selectByParamsForFront() /data/www/src/app/Model/Svc/Category.php:214
...
[0x00007fecbd613580] getEsData() /data/www/src/app/Controller/Front/ListController.php:26
[0x00007fecbd613400] indexAction() /data/www/vendor/andals/vine/src/Framework/App/Web.php:107
[0x00007fecbd613380] call_user_func_array() /data/www/vendor/andals/vine/src/Framework/App/Web.php:107
[0x00007fecbd613290] runController() /data/www/vendor/andals/vine/src/Framework/App/Web.php:73
[0x00007fecbd6131b0] handleRequest() /data/www/vendor/andals/vine/src/Framework/App/Web.php:48
[0x00007fecbd6130f0] run() /data/www/src/run/front/index.php:6

可以看到最终是执行SQL的时候很慢,上管理平台查看发现在报警的两个时间点,MySQL从库的QPS突然降到0而主库QPS突然大幅升高,于是连忙反馈给DBA同学。

DBA同学排查后发现,当前读写量比较大,且有个新增字段的改标语句正在运行,停止后问题恢复;而主从库QPS的突变是因为从库延时大被Proxy操作下线了。

我们梳理后发现,当前有个Task程序在批量往数据库里导数据,表里的数据较多(千万级),这种情况下改表导致数据库响应变慢;同时页面上有个查询没有加缓存,SQL语句执行超时设置得也有问题,最终导致PHP-FPM进程都被卡住了,没有空闲进程来处理LVS健康检查的页面,出现了LVS下RS机器全下线的问题。

事后,我们对发现的问题做了修复,并在确保没有大量访问的情况下提交了改表操作,改表顺利执行完成。

wok和kimchi – kvm虚拟机网页管理

先安装wokd服务

1、解决依赖

yum install gcc make autoconf automake gettext-devel git rpm-build libxslt 
python-cherrypy python-cheetah PyPAM m2crypto 
python-jsonschema python-psutil python-ldap 
python-lxml nginx openssl python-websockify 
fontawesome-fonts logrotate python-ordereddict

2、下载rpm包

wget https://github.com/kimchi-project/wok/releases/download/2.5.0/wok-2.5.0-0.el7.centos.noarch.rpm

3、安装wokd

rpm -ivh wok-2.5.0-0.el7.centos.noarch.rpm

4、启动wokd服务

systemctl daemon-reload
systemctl start wokd

安装kimchi

1、解决依赖

yum install gcc make autoconf automake gettext-devel git rpm-build libxslt 
libvirt-python libvirt libvirt-daemon-config-network 
qemu-kvm python-ethtool sos python-ipaddr nfs-utils 
iscsi-initiator-utils pyparted python-libguestfs 
libguestfs-tools novnc spice-html5 
python-configobj python-magic python-paramiko 
python-pillow python-ordereddict

重启libvirtd服务:

systemctl restart libvirtd

2、下载rpm包

wget https://github.com/kimchi-project/kimchi/releases/download/2.5.0/kimchi-2.5.0-0.el7.centos.noarch.rpm

3、安装kimchi

rpm -ivh kimchi-2.5.0-0.el7.centos.noarch.rpm

启动服务:

关闭selinux:

setenforce 0

重启wokd服务:

systemctl restart wokd

启动nginx服务:

systemctl start nginx

关闭网络自动管理

systemctl stop NetworkManager

通过浏览器访问:

https://IP_ADDR:8001

wokd默认通过PAM方式来验证,因此可以直接使用主机上的用户账号登录。

ubuntu-14.04下安装KVM虚拟化

ubuntu-server 上安装KVM

一、检查CPU是否支持安装 KVM

egrep -o '(vmx|svm)' /proc/cpuinfo

二、安装KVM所需要的软件包

apt-get install qemu-kvm libvirt-bin virt-manager bridge-utils

注:virt-manager为GUI管理窗口,bridge-utils:用于网络桥接

三、检查安装是否成功

lsmod | grep kvm

四、拉取镜像

wget http://mirrors.ustc.edu.cn/ubuntu-releases/trusty/ubuntu-14.04.4-server-amd64.iso
qemu-img create -f raw /data1/kvm/data/kvm-20160724-01.img 1024G

五、网卡配置参照

cat > /etc/network/interfaces <<OEF
# The loopback network interface
auto lo
iface lo inet loopback

auto eth0
iface eth0 inet manual

auto eth1
iface eth1 inet manual

auto br0
iface br0 inet static
address 118.123.9.86
netmask 255.255.255.192
gateway 118.123.9.65
dns-nameservers 144.114.114.114
bridge_ports eth0

auto br1
iface br1 inet static
address 10.10.20.16
netmask 255.255.255.0
gateway 10.10.20.254
bridge_ports eth1
OEF

六、虚拟机安装

virt-install --name kvm-20160725-01     --ram 8196     --vcpus=8,sockets=1,cores=8,threads=1     --arch=x86_64     --os-type=linux     --os-variant=ubuntutrusty     --accelerate --cdrom /data8/kvm/images/ubuntu-14.04.4-server-amd64.iso     --boot menu=on     --disk path=/data8/kvm/data/kvm-20160725-01.img,size=500,cache=writeback,device=disk     --network network=default,model=virtio     --graphics vnc,port=-1,listen=0.0.0.0

kvm iptables转发

#!/bin/bash
pro='tcp'
NAT_Host='125.65.43.197'
NAT_Port=3000
Dst_Host='10.10.20.100'
Dst_Port=22
iptables -t nat -A PREROUTING  -m $pro -p $pro  --dport $NAT_Port -j DNAT --to-destination $Dst_Host:$Dst_Port
iptables -t nat -A POSTROUTING -m $pro -p $pro --dport $Dst_Port -d $Dst_Host -j SNAT --to-source $NAT_Host
#iptables -t nat -A POSTROUTING -j MASQUERADE
iptables -I FORWARD -d 192.168.122.0/24 -m state --state NEW,RELATED,ESTABLISHED -j ACCEPT

克隆:

virt-clone --original ubuntu-14-tmp --name kvm-20160715-01 --file /data1/kvm/data/kvm-20160715-01.img

在线添加网卡:

virsh attach-interface --domain kvm-20160725-01 --type bridge --source br0 --model virtio --config --live

进入virt-manager

在桌面版的命令窗出入:virt-manager

快速建虚拟机:

修改配置文件:

sudo vim /opt/cd-image/isolinux/isolinux.cfg
# D-I config version 2.0
include menu.cfg
default vesamenu.c32
prompt 0
timeout 1
ui gfxboot bootlogo
sudo vim /opt/cd-image/preseed/ubuntu-bbd-installer.seed 

截取修改的部分

# Static network configuration.
d-i netcfg/get_nameservers string 10.0.0.1
d-i netcfg/get_ipaddress string 10.0.0.171  (只修改此行,为虚拟机ip)
d-i netcfg/get_netmask string 255.255.255.0
d-i netcfg/get_gateway string 10.0.0.1
d-i netcfg/confirm_static boolean true

# from being shown, even if values come from dhcp.
d-i netcfg/get_hostname string bbdtao01 (虚拟机主机名)
d-i netcfg/get_domain string

镜像制作:

mkisofs -r -V "BBD Server Install CD" -cache-inodes -J -l -b isolinux/isolinux.bin -c isolinux/boot.cat -no-emul-boot -boot-load-size 4 -boot-info-table -o ubuntu-bbd-installer-bbdhdp02-net162.iso /opt/cd-image/

创建虚拟机:

virt-install --name bbdhdp02 --hvm --ram 20480 --vcpus 2 --disk path=/data/kvm/bbdhdp02.qcow2,size=100 --bridge=br0 --accelerate --vnc --vncport=5982 --cdrom /home/bbd/ubuntu-bbd-installer-bbdhdp02-net162.iso -d

网址信息:

http://zhidao.baidu.com/link?url=inrg0yVfsi4JF_Okeq06FnMCZDGWUSwoJaK0LwxR7wne4-3hcaxiAFk0fod1DYNnFEbF2VIA9_4Fx1LhO9Z8fdjYMJyPlO8HgTsqQFe-JjS
http://www.linuxidc.com/Linux/2011-03/33653p3.htm
https://wiki.xargs.cn/wiki/doku.PHP/linux:kvm:kvm%E6%89%8B%E5%86%8C

Kubernetes DaemonSet的滚动升级

DaemonSet好比Kubernetes集群Node的守护进程,可以保证在每个Node上(或者一部分Node上)都运行同一个Pod。 目前我们的线上环境主要用到以下两个DaemonSet:

  • kube-flannel-ds 这个是部署Kubernetes集群时选用的是flannel network add-on
  • fluent-bit 这个是用来在部署在各个Node上,收集各个Node上容器的日志。我们选用的日志收集方案是EFK(Elasticsearch+Fluent-bit+Kibana),后边有时间再写点fluent-bit的内容

我们目前线上Kubernetes的版本总是落后最新的release版本,例如现在Kubernetes最新是1.7,我们使用1.6.x。但是我们注意到Kubernetes 1.7中很多外部组件、Addon都做了更新。我们在使用1.6.x的过程中会考虑提前升级这些组件,以便于后续顺利将Kubernetes升级到1.7。DaemonSet的升级就是需要考虑的。

滚动升级特性是Kubernetes服务发布的一个很有用的特性,而Kubernetes 1.6+支持DaemonSet的滚动升级,1.7开始支持DaemonSet滚动升级的回滚。下面一起来学习一个DaemonSet的滚动升级。

DaemonSet的升级策略

DaemonSet目前有两种升级策略,可以通过.spec.updateStrategy.type指定:

  • OnDelete: 该策略表示当更新了DaemonSet的模板后,只有手动删除旧的DaemonSet Pod才会创建新的DaemonSet Pod
  • RollingUpdate: 该策略表示当更新DaemonSet模板后会自动删除旧的DaemonSet Pod并创建新的DaemonSetPod

体验DaemonSet的滚动升级

要使用DaemonSet的滚动升级,需要 .spec.updateStrategy.type设置为RollingUpdate。 也可以进一步设置.spec.updateStrategy.rollingUpdate.maxUnavailable,默认值为1; 设置.spec.minReadySeconds默认为0,用于指定认为DaemoSet Pod启动可用所需的最小的秒数。

下面以我们测试环境中的flannel DaemonSet为例,体验从0.7.1升级到0.8.0的过程。

之前0.7.1的manifest文件没有设置更新策略为滚动升级,首先设置一下,同时注意修改flannel镜像版本为0.8.0:

apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
  name: kube-flannel-ds
  namespace: kube-system
  labels:
    tier: node
    app: flannel
spec:
  updateStrategy:
        type: RollingUpdate
  template:
  ......

然后执行:

kubectl apply -f kube-flannel.yml

可以使用下面的命令查看滚动升级状态:

kubectl rollout status ds/kube-flannel-ds -n kube-system
Waiting for rollout to finish: 1 out of 4 new pods have been updated...
Waiting for rollout to finish: 2 out of 4 new pods have been updated...
Waiting for rollout to finish: 3 out of 4 new pods have been updated...
daemonset "kube-flannel-ds" successfully rolled out

缩减docker镜像大小

镜像是 Docker 运维的基本单元。 优化镜像体积,能够:

  • 缩短部署时的下载时间;

  • 提升安全性,因为可供攻击的目标更少;

  • 减少故障恢复时间;

  • 节省存储开销。

正确认识分层和共享

认清与理解 Docker 镜像的层次结构,是进行镜像优化的前提和基础。

分层

docker 镜像的存储结构是分层次的。 无论底层的文件系统(可选可配置)是基于快照还是分块。

镜像构建的过程里,每步操作都产生一个只读的层:可重用,可复制,但是不可修改。 层次叠加,形成了包含历史(history)结构的镜像。 镜像启动后,产生了容器(container)。容器也是一个单独的层,可读写,但是不持久化,易丢失。

共享

共享,以分层为基础。 以一个镜像为祖先,分别用多种方式做不同操作,各自生成新层,则形成多个新的镜像,祖先镜像则成为共享的部分。 继承自共同的祖先,实现了对共同内容的利用。

未分类

镜像优化

对单个镜像体积做优化,采取方式有两类思路:

  • 选择尽可能小的基础镜像

  • 打包尽可能少的内容入镜像

    • 如:去除或减少编译、测试等中间步骤内容

    • 使用单行命令

工程实践中,还要考虑多镜像复用,尽量:

  • 将能复用的部分放进模版镜像。

选择最小化基础镜像,创建模版镜像

基础镜像需要满足的基本条件:

  • 正确的 init 系统。(discuss)

  • 日志。Docker 的应用程序日志一般默认在标准输出,而系统进程仍然会往 /dev/log 写数据。需要有日志处理程序 syslog

  • 后台任务如 cron

  • 工具进程如 sshd(慎重选择)

这些需求,与在传统的服务器和虚拟机上做部署,是相似的。

如果仅仅基于这种相似性,就选择了使用 CentOS/Debian/Ubuntu 做为基础镜像,那么就有问题了。 首先,这些传统发行版的 Docker 镜像并不能符合基础需求; 其次,这些发行版的体积太大。

所有,仍然需要选择较小的发行版制作初始镜像。

市面上可选的有:

  • phusion/baseimage

  • baseimage-amzn

  • Alpine

  • busybox

使用共同的模版镜像

如果有多个业务需要运维,则在公共基础镜像基础上,构建私有的模版镜像。 共同的模版镜像,能够:

  • 实现镜像共享

  • 减少重复工作。解决边缘问题

  • 减少开发时间。专注于上层应用

  • 减少编译时间。

  • 减少部署时间。基于镜像的层共享

模版镜像的目标是抽取业务的公共部分。

持续的审查和优化

上线后,需要经常关注这些问题:

  • 某个组件放在模版镜像中,现在已经很少用到了?或者反之。

  • 新业务使用到某个公共镜像,效果还不错,要不要推而广之?

解决这些问题,还需要经常复审镜像服用的效率。 简单的方案,可以对各类环境中的模版使用情况进行统计,尽量合并相似的镜像,将使用率较高的组件吸收进模版镜像。

常用考评指标:

  • 开发效率

  • 编译效率

  • 传输效率

  • 存储效率

要不要使用 –flatten

–flatten 是现在还是一个测试特性,可以减少镜像总体积,然而与复用的原则有冲突。 需要更加实际情况去做考虑。

  • 要不要使用 –flatten

为什么选择最小的基础镜像(如alpine)之后,应用镜像反而变大了?

这是因为应用程序需要一系列依赖。 如果使用最小的基础镜像,在制作应用镜像时,比如需要去安装依赖。 安装步骤可能会引入更多不必要的元素。 所以反而体积会超过已包含所需依赖的公共镜像。

centos -> app

alpine -> dependencies -> app  # Too much dependencies

ubuntu16.04使用nginx、uwsgi部署django应用

安装相关软件

更新源

sudo apt-get update 
sudo apt-get upgrade

安装虚拟环境

sudo apt-get install Python-virtualenv

安装nginx

sudo apt-get install nginx

安装uwsgi

sudo pip3 install uwsgi

或者

sudo pip3 install https://projects.unbit.it/downloads/uwsgi-lts.tar.gz

安装uwsgi出错就安装

sudo apt-get install python-dev 
sudo apt-get install python3.5-dev

本文用到的软件环境

  • ubuntu 16.04
  • Python 3.5.2
  • Django 1.11.4
  • nginx 1.10.3
  • uwsgi 2.0.15

创建一个普通用户

创建用户组webapps

sudo groupadd –system webapps

创建用户user_test并设置密码

sudo useradd –system –gid webapps –shell /bin/bash –home /webapps/test user_test

passwd user_test

创建用户目录并修改目录权限

mkdir -p /webapps/test

chown user_test /webapps/test/

添加用户user_test sudo权限

在/etc/sudoers添加

user_test ALL=(ALL:ALL) ALL

创建测试应用

切换用户为user_test

su - user_test

cd /webapps/test

创建python虚拟环境,这里指定为3.5版本,并激活

virtualenv -p /usr/bin/python3.5 .

source bin/activate

安装django

pip install django

创建django工程mysite

django-admin startproject mysite

创建应用app

cd mysite

django-admin startapp app

修改mysite/settings.py

将外网ip添加到ALLOWED_HOSTS

ALLOWED_HOSTS = ['xxx.xxx.xxx.xxx', '127.0.0.1']

在INSTALLED_APPS里追加 ‘app’

新建app/static目录

在mysite/settings.py里添加

STATIC_ROOT = os.path.join(BASE_DIR, 'app', 'static')

收集静态文件到app/static目录

python manage.py collectstatic

迁移数据库

python manage.py migrate

创建后台管理用户

python manage.py createsuper

添加视图函数

在app/views.py添加

from django.http import HttpResponse
def index(request):
    return HttpResponse('<h1>Hello World!</h1>')

在mysite/urls.py添加

from app import views
urlpatterns = [
    ...
    url(r'^$', views.index),
]

测试django应用运行是否正常

python manage.py runserver

使用curl命令访问

curl 127.0.0.1:8000

正常会输出 <h1>Hello World!</h1>

Ctrl C 退出

当前所涉及的一些目录

  • 用户目录:/webapps/test
  • 虚拟环境目录: /webapps/test
  • 工程目录:/webapps/test/mysite

配置ngnix

从https://github.com/nginx/nginx/blob/master/conf/uwsgi_params下载到/etc/nginx/conf/目录

新建mysite/test_ngnix.conf文件,内容如下

# the upstream component nginx needs to connect to
upstream testapp {
    server unix:///webapps/test/mysite/mysite.sock; # for a file socket
    #server 127.0.0.1:9090; # for a web port socket (we'll use this first)
}

# configuration of the server
server {
    # the port your site will be served on
    listen      80;
    # the domain name it will serve for
    server_name localhost; # substitute your machine's IP address or FQDN
    charset     utf-8;

    # max upload size
    client_max_body_size 75M;   # adjust to taste
    access_log      /webapps/test/mysite/nginx_access.log;
    error_log       /webapps/test/mysite/nginx_error.log;

    # Django media
    location /media  {
        alias /webapps/test/mysite/app/media;  # your Django project's media files - amend as required
    }

    location /static {
        alias /webapps/test/mysite/app/static; # your Django project's static files - amend as required
    }

    # Finally, send all non-media requests to the Django server.
    location / {
        uwsgi_pass  testapp;
        include     /etc/nginx/conf/uwsgi_params; # the uwsgi_params file you installed
    }
}

将test_ngnix.conf链接到/etc/nginx/sites-enabled/目录

sudo ln -s /webapps/test/mysite/test_ngnix.conf /etc/nginx/sites-enabled/

记得删除自带的default文件,否则输入外网ip看到的是ngnix欢迎页

sudo rm /etc/nginx/sites-enabled/default

这里用到的是unix file socket

所以把用户www-data加到webapps用户组,否则读取socket文件时会有权限问题

sudo usermod -a -G webapps www-data

重启ngnix

sudo /etc/init.d/nginx stop 
sudo /etc/init.d/nginx start

配置uwsgi

新建wsgi.ini, 内容如下

[uwsgi]
#socket = 127.0.0.1:9090
chdir=/webapps/test/mysite
module=mysite.wsgi
master = true
processes=2
threads=2
max-requests=2000
chmod-socket=664
vacuum=true
daemonize = /webapps/test/mysite/uwsgi.log
virtualenv = /webapps/test
gid=webapps
uid=user_test
socket = /webapps/test/mysite/mysite.sock

运行uwsgi

uwsgi –ini wsgi.ini

用浏览器访问外网ip,正常会显示Hello World

使用uwsgi Emperor mode

# create a directory for the vassals
sudo mkdir /etc/uwsgi
sudo mkdir /etc/uwsgi/vassals
# symlink from the default config directory to your config file
sudo ln -s /webapps/test/mysite/wsgi.ini /etc/uwsgi/vassals/
# run the emperor
sudo uwsgi --emperor /etc/uwsgi/vassals --uid user_test --gid webapps

Redis报错-ERR max number of clients reached

Redis报错redis报错 ERR max number of clients reached错误
我看啦一下连接数有500多个,可能是因为客户端接入太多
设置同一时间最大客户端连接数,默认无限制,Redis可以同时打开的客户端连接数为Redis进程可以打开的最大文件描述符数,如果设置 maxclients 0,表示不作限制。当客户端连接数到达限制时,Redis会关闭新的连接并向客户端返回max number of clients reached错误信息

解决方案

更改redis.conf配置文件

maxclients 1000

未分类

Linux系统下通过bash shell脚本实现倒计时的方法

本文主要讲述如何在linux系统下通过bash shell 脚本来实现在屏幕上输出倒计时的方法。

先来看看实现后的脚本,如下:

#!/bin/bash
 # script name: ctimer.sh
 # Author: osetc.com
 # --------------------------------------------------------

row=2
 col=2
 countdown() {
 msg="starting..."
 clear
 tput cup $row $col
 echo -n "$msg"
 l=${#msg}
 l=$(( l+$col ))
 for i in {30..1}
 do
     tput cup $row $l
     echo -n "$i"
     sleep 1
 done
 }
 countdown

首先我们定义了一个名为countdown的shell 函数,在函数里定义了一个msg变量用于在屏幕上显示倒计时信息,clear 命令用于清除屏幕上的历史输出信息,tput cut 命令用于设置屏幕输出信息的位置,最后通过for循环来实现倒计时,并更新输出信息的位置。

执行上面的脚本

$bash ctimer.sh
 Starting...30

如何恢复丢弃的 git stash 数据

不要让 git 命令中的错误抹去你数天的工作

今天我的同事几乎失去了他在四天工作中所做的一切。由于不正确的 git 命令,他把保存在 stash[1] 中的更改删除了。在这悲伤的情节之后,我们试图寻找一种恢复他所做工作的方法,而且我们做到了!

首先警告一下:当你在实现一个大功能时,请将它分成小块并定期提交。长时间工作而不做提交并不是一个好主意。

现在我们已经搞定了那个错误,下面就演示一下怎样从 stash 中恢复误删的更改。

我用作示例的仓库中,只有一个源文件 “main.c”,如下所示:

未分类
Repository with one source file

它只有一次提交,即 “Initial commit”:

未分类
One commit

该文件的第一个版本是:

未分类
First version of the file

我将在文件中写一些代码。对于这个例子,我并不需要做什么大的改动,只需要有什么东西放进 stash 中即可,所以我们仅仅增加一行。“git diff” 的输出如下:

未分类
git-diff output

现在,假设我想从远程仓库中拉取一些新的更改,当时还不打算提交我自己的更改。于是,我决定先 stash 它,等拉取远程仓库中的更改后,再把我的更改恢复应用到主分支上。我执行下面的命令将我的更改移动到 stash 中:

git stash

使用命令 git stash list 查看 stash,在这里能看到我的更改:

未分类
Output of changes in our stash

我的代码已经在一个安全的地方,而且主分支目前是干净的(使用命令 git status 检查)。现在我只需要拉取远程仓库的更改,然后把我的更改恢复应用到主分支上,而且我也应该是这么做的。

但是我错误地执行了命令:

git stash drop

它删除了 stash,而不是执行了下面的命令:

git stash pop

这条命令会在从栈中删除 stash 之前应用它。如果我再次执行命令 git stash list,就能看到在没有从栈中将更改恢复到主分支的之前,我就删除了它。OMG!接下来怎么办?

好消息是:git 并没有删除包含了我的更改的对象,它只是移除了对它的引用。为了证明这一点,我使用命令 git fsck,它会验证数据库中对象的连接和有效性。这是我对该仓库执行了 git fsck 之后的输出:

未分类
Output after executing the git-fsck command on the repository

由于使用了参数 –unreachable,我让 git-fsck 显示出所有不可访问的对象。正如你看到的,它显示并没有不可访问的对象。而当我从 stash 中删除了我的更改之后,再次执行相同的指令,得到了一个不一样的输出:

未分类
Output after dropping changes on stash

现在有三个不可访问对象。那么哪一个才是我的更改呢?实际上,我不知道。我需要通过执行命令 git show 来搜索每一个对象。

未分类
Output after executing the git-show command

就是它!ID 号 95ccbd927ad4cd413ee2a28014c81454f4ede82c 对应了我的更改。现在我已经找到了丢失的更改,我可以恢复它。其中一种方法是将此 ID 取出来放进一个新的分支,或者直接提交它。如果你得到了你的更改对象的 ID 号,就可以决定以最好的方式,将更改再次恢复应用到主分支上。对于这个例子,我使用 git stash 将更改恢复到我的主分支上。

git stash apply 95ccbd927ad4cd413ee2a28014c81454f4ede82c

另外需要重点记住的是 git 会周期性地执行它的垃圾回收程序(gc),它执行之后,使用 git fsck 就不能再看到不可访问对象了。