开发Telegram bot的几点心得

Telegram bot 可以是一个HTTPS服务(Webhook模式:被动push Update)或者daemon(getUpdates模式:主动pull Update),不断的从bot API server获得Update,进行处理,做出动作,而实现一些功能。

Update解析

Telegram bot收到的Update对象有个不好的地方,就是它没有自带type属性。为了判断该Update到底是什么类型,需要先试探性的检查几个特定属性是否在Update内,然后再dispatch给各个type特定的处理函数。

输入控制

考虑到bot需要处理用户私聊和群聊两种场景,在获得update之后马上进行分类是必须的。

chat id == from id的情况下,bot收到的消息是私聊的。这种情况下一般消息上不会带有at bot的标志,不过偶尔也有人手贱非要输入,建议先无脑过滤掉,再进一步处理。此类bot大部分都是查询/控制类功能的bot,需要判断:发消息的人是否有权限对我发消息进行查询和控制

chat id != from id的情况下,bot收到的消息是群聊,chat id是群的ID号,from id是发消息人的ID号。如果bot被设计为处理群里的消息,在此时需要进行进一步判断:

  • 消息是来自我乐意管理的群吗?
  • 是普通消息还是botcommand消息?(对于一些群管机器人的发言积分、自动回复功能,一般是不需要特地at bot的,也不需要使用botcommand格式)
  • 如果是botcommand消息,发消息的人有权限吗?

响应

虽然Telegram在webhook模式提供了
Making requests when getting updates 功能,但似乎一次只能返回一个动作。考虑到业务的复杂性,有时候需要做出多个动作,比如群管机器人的删除/封禁动作,既要做出删除封禁动作,还要给管理员回复一个消息,甚至有时候需要延迟一段时间再做动作等等……还是建议直接调用bot API实现功能,而不要使用在返回体中调用API的方法。这玩意,也就聊胜于无吧。

限制:forwardMessage处理多图

一个经常遇到的需求就是:监听群,收到某种消息之后就自动转发到另一处。但是一旦遇到这个消息含有多个图,情况就会变得糟糕,人类看来是一个消息带了多个图,但在bot API里会被分拆成多个Update,其中第一个图和文字组成第一个Update,其它的每个图分别一个Update,它们拥有相同的media_group_id号码,似乎可以相互关联;

单从Updates的角度来看,你需要等到下一个不带media_group_id号码的消息,或者拥有不同的media_group_id号码的消息,才能确定上次的一组media已经收集齐全了,然后才能用forwardMessages一次性转发把这堆内容转发过去,这样它在目标chat里才会正确的粘连起来;如果还没有凑齐就调用forwardMessages,则多次forwardMessages转发过去的消息之间并不会自动粘连。

但问题是:并没有一个字段声明这个media group里到底有多少个内容,也没有任何情报指出Telegram保证把这一堆Update放在同一个Updates里,尤其是:Webhook模式一次只能收到一个Update、getUpdates模式你指定的limit说不定会小于拆分的瓣数,甚至拆出来的好多瓣有可能是bot的不同实例分别处理的。

再加上绝大多数bot的写作方式都是循环一圈仅处理一条Update,导致:凑齐media group里所有内容这个需求存在工程上的困难

Posted in 默认分类 | Tagged , | Leave a comment

在Python里抑制requests库的日志消息

我自己经常在自己的脚本开头使用logging.basicConfig(level=logging.DEBUG)初始化logging库,但是随之而来的就是requests会输出大量日志,甚至盖过了我自己的内容。所以我打算抑制requests的日志。

综合搜索的资料,和探索源代码,我发现:

  • 其实requests代码里根本就没有调用任何logging/logger,甚至它还在__init__.py里给“自己的”logger设置了NullHandler
  • docs/api.rst 文档里其实讲了怎么“配置”日志,只是没有“supress”这个词,以至于我没搜到
  • 通过在Format里加上%(name)s,可以发现写日志的其实是urllib3.connectionpool

所以只需要在basicConfig后面加一句

logging.getLogger(“urllib3”).setLevel(logging.WARNING)

就可以抑制这部分日志了。

另外,logging.Logger.manager.loggerDict是个好东西,可以检查当前到底存在哪些logger。通过检查loggerDict[‘urllib3.connectionpool’].propagate发现其为True,其上层也是True,因此,虽然这两层logger一个没handler,一个NullHandler,但是该logger记录的日志消息仍会逐层上传,最终被basicConfig的root logger处理。

Posted in 默认分类 | Tagged , , | Leave a comment

GNU和BSD版本的xargs 分隔符不同

例子:
list="a b c d e"; echo $list |xargs -n1 -I{} echo begin {} end

在Mac上执行结果:
begin a end
begin b end
begin c end
begin d end
begin e end

在Linux上执行结果:
begin a b c d e end

我这里的需求是有一堆输入,要分别以其为参数,执行一些命令,无论是否成功都要对所
有目标执行,所以
1 “一些命令”我选用shell function来实现,在其中读了$1作为本次处理的目标
2 “所有目标”我选用xargs;如果选Parallel还得额外安装

结果发现xargs在切分“以空格为分隔符”的字符串的时候,GNU版本默认不切分,结果把
整个“含空格分隔符的字符串”传给函数,执行了一次,而函数里又选了$1作为本次执行
目标,其综合结果就是只对列表中第一个目标执行了一遍

更惨的是我对比的时候是在Mac上做的对比,怎么看怎么顺眼……

最后请教同事,用xargs的-d参数解决的

   This  manual page documents the GNU version of xargs.  xargs reads items from the standard input, delimited by blanks (which can be protected with double or single quotes or a backslash) or newlines

GNU xargs的manpage说支持blanks 按说空格也应该可以啊……
xargs.c的read_line函数里:

  893           /* POSIX: In the POSIX locale, the separators are <SPC> and 
  894            * <TAB>, but not <FF> or <VT>. 
  895            */ 
  896           if (!bc_ctl.replace_pat && ISBLANK (c)) 
  897             { 
  898               *p++ = '\0'; 
  899               len = p - linebuf; 
  900               if (EOF_STR (linebuf)) 
  901                 { 
  902                   eof = true; 
  903                   return first ? -1 : len; 
  904                 } 
  905               bc_push_arg (&bc_ctl, &bc_state, 
  906                            linebuf, len, 
  907                            NULL, 0, 
  908                            initial_args); 
  909               p = linebuf; 
  910               state = SPACE; 
  911               first = false; 
  912               continue; 
  913             } 

这一段状态机代码应该是“之前读到了普通字符,这次读到了空格”的处理,这时候应该把已经读到的这一段作为一个参数加到列表里去 

看它的判断条件if (!bc_ctl.replace_pat && ISBLANK (c)) 
其实是要求没用-i/-I参数,且本次读到的字符为空白

验证一下,去掉-i之后:

echo a b c d e |xargs -n1  echo begin {} end 
运行结果就几乎正确了。虽然丧失了使用占位符的能力,但至少它确实按照空格进行分割了 
begin {} end a 
begin {} end b 
begin {} end c 
begin {} end d 
begin {} end e 
  
我觉得这个判断条件就是个bug。但是有网友指出:按照POSIX标准、GNU xargs的文档,开启-I就是强制一整行的,我的用法不清真。对此我只能说:满足标准但是不满足需求啊,为什么输出端的参数会影响输入端的行为呢?

Posted in 默认分类 | Tagged | Leave a comment

昨天遇到collectd exec插件的bug,顺便发现他们不按套路出牌啊

先说症状:

collectd exec插件调用的几个外部脚本,其中总会随机有一个缺少COLLECTD_HOSTNAME和COLLECTD_INTERVAL环境变量。

搜了一下是这个bug https://github.com/collectd/collectd/issues/3041

然后我好奇啊,就读了一下修改前后的代码,发现collectd不按套路出牌。

带有bug的版本:

先setenv()设置主进程自己的环境变量,然后尝试fork(),如果成功,在子进程里execvp();主进程重新unsetenv()恢复主进程自己的环境变量。在多个exec密集执行的时候,都会访问主进程的环境变量,会有race condition,偶尔会发生前一个exec插件刚unsetenv()然后后一个exec插件开始fork()的情况,丢失环境变量。

修复后的版本:

先fork(),在子进程里准备环境变量数组,尝试execvpe()带签署环境变量数组作为参数,执行新进程(execvpe()为GNU专有扩展),或者先设置extern char **environ指针指向准备好的数组,然后execvp()执行新进程直接继承。

别人家套路:

先准备环境变量数组,然后fork(),在子进程里execve()并使用前述环境变量数组作为参数。

Posted in 默认分类 | Tagged , | Leave a comment

Tencent tlinux的$releasever比Redhat原版更反动

前几年我写过一篇批判$releasever的博客之后,现在工作中又用到了tlinux,而且发现它的$releasevar居然是2.2,是带小数的。但是看了看tlinux-release的主版本号确实是2,不带小数;它provide的centos-release的主版本号也是7,也不带小数。

折腾了一番,发现yum的config.py里,class VersionGroupConf下边的_read_yumvars(yumvars, root) 函数,直接提供了用 /etc/yum/vars/ 下边的文件来设置release属性的方法。

Posted in 默认分类 | Leave a comment

supervisor泄漏进程案例分析

起因

前几天使用 salt ‘*’ test.ping 的时候发现响应内容中有一些“某某minion was already deleted from tracker, probably a duplicate key“的提示信息。刚开始误以为是salt-key管理有问题,尝试删除再重新accept,但是依然会出错。到该minion上检查,发现上面运行了两套salt-minion*三层进程树,一共6个进程,其中一套的PPID为1,另一套的Parent是supervisord。

然后就开始研究这种情况是怎么产生的,发现有两种可能:

第一种可能

supervisor本身不被systemd监管,被SIGKILL信号杀死时,因为SIGKILL由内核直接处理,所以并没有机会关闭下属的进程,导致下属salt-minion进程树泄漏。而且不但salt-minion进程树泄漏,连同样被supervisor监管的另一个服务也一并泄漏,二者的PPID都变成了1号。

不过,如果supervisor本身被systemd监管,在其主进程被杀死时,systemd会给整个service slice cgroup里所有进程补刀,所以并不会泄漏进程;如果supervisor是被SIGTERM信号杀死,它也会给下属子进程发信号,一般也不会泄漏进程。

第二种可能

supervisor没有受到影响,正常运行;supervisor监管的salt-minion三层进程树的其中最高层进程(也就是supervisord的直属子进程)被SIGKILL信号杀死,随即,第二层进程exit(1) (不明原因,可能需要看一下salt-minion源码),导致第三层进程变成孤儿。经检查源代码的_spawn_as_child()函数,supervisor针对其监管下的每一个服务,都是采用 fork() + setpgid() +execve() 的方式来启动的,在调用setpgid()改变了process group id之后,第三层进程的孤儿收养关系就不再归属于supervisord进程,而是归属于1号进程。

随后supervisor会重启salt-minion服务,产生新的3个进程,加上之前剩下的,一共4个。

结论

  • 考虑到观察到6个进程而不是4个,实际发生的大概是前一种情况
  • supervisor虽然有“能力”处理进程退出之后马上重启的工作,但是因为使用了setpgid()把下属服务与自己隔离,没使用cgroup机制把下属服务单独圈起来,又不具备1号的神圣地位,其实它并不知道到底下属了多少、哪些进程,从机制原理上就根本无法保证所有下属的孤儿进程都被其reap。还是建议不要在严肃场合使用
  • 1号进程神圣,所有的服务进程监管工作都应该交给1号进程来处理
Posted in 默认分类 | Tagged , , | Leave a comment

滥用crond触发systemd-login故障一例

故障现象

2021年1月20日接到通知,要把systemd升级到219-73.tl2.10或以上、并把rsyslog一起升级,以修复/var/log/messages无日志内容的bug。经实验,发现使用yum升级两个软件包之后,systemd-logind的可执行文件也被更新,导致该服务处于原可执行文件已删除的状态,所以我提议,在升级步骤中增加重启systemd-logind服务的动作。在Ansible playbook里,因为不能表达“大于219-73.tl2.10“这种范围型版本号,所以就明确指定systemd的版本为当前yum能自动安装到的最新版本219-78.tl2.3

2月1日由同事执行更新操作之后,大部分节点都正常工作,但有两台发生重启事故,另有一台上的 35777 systemd-login进程占内存高达4~6G。这三台恰好是一组elasticsearch的三台master节点,均为C8机型,即16G内存的kubernetes容器。

检查修复

我尝试重启剩余的这台的system-logind,发现新进程3851号仍然占6G内存。查看/proc/3851/smaps,该区域为heap;用pmap命令查看,显示为[ anon ]。对比正常服务器的同一个内存区域,才244K而已。


检查三台故障机及其宿主机的日志,发现大量oom记录,其中重启的两台所属宿主机的kubelet也发生故障重启:
Feb 1 18:43:50 TENCENT64 kubelet: panic: runtime error: invalid memory address or nil pointer dereference

先gcore一份保留故障现场。
由于操作系统组同事不登录上来观察,仅提供重启进程等建议,我只好自己做检查。
根据建议,检查了dbus服务(dbus-daemon进程),发现也是可执行文件被删除的状态。检查yum日志,发现在去年6月升级了dbus包,但是服务进程是3月5日启动的,也就是升级包的时候并没有重启这个服务。
再次尝试重启systemd-logind,新进程14278号,发现用内存VmPeak: 5270484 kB;但是过了一会儿再观察,发现增加到了VmPeak: 6599828 kB。这说明内存的增长是一个过程,虽然增长比较快,但并不是一下子就6G的。于是我决定strace一下它。

先关闭systemd-logind服务。使用命令strace -ff -s 1000 -p 1挂在systemd主进程上做跟踪,并用-o参数把多个进程的跟踪记录分别写在文件里。然后启动systemd-logind服务。这样,strace可以跟踪到 1号进程clone+execv执行systemd-logind的瞬间,以及systemd-login最开头的行为。
检查systemd-login的strace记录,发现大量访问 /run/systemd/session/ 目录下面文件的动作。检查该目录,发现大量残留文件。
搜索,发现 https://www.jianshu.com/p/343a072e2521 、https://github.com/systemd/systemd/issues/1961 等内容,遂决定用systemctl stop命令清理这些残留的session scope。清理之后再重启systemd-logind服务,恢复正常。

为了验证这个问题,再次拿出之前的gcore,查看指定地址,发现大部分数据为0,个别位置稀稀拉拉的确实发现一些/run/systemd/session/下面的文件名等字符串,但是浓度极低,缺乏作为线索的价值。

原因分析

查看/run/systemd/session/下面残留的session文件,发现绝大部分都有SERVICE=crond这一行。检查crontab发现,腾讯内部常用的山寨启动服务和山寨看守进程的方式(即在cron里ps查看,如果进程消失就再次启动)导致了elasticsearch的java进程被计入user session。在219版本systemd的logind.conf配置文件里,KillUserProcess默认值为no(注意systemd后续某版本的默认值变成了yes!!!),man logind.conf没多说什么,但是新版本的man logind.conf说设置KillUserProcess=no会导致 user session scope保持在abandon状态,即我们遇到的这种症状。
所以,山寨的看守进程方式,是导致本次故障的根本原因。这种方式不仅内部,连对外服务的腾讯云也有类似问题:https://www.jianshu.com/p/343a072e2521

这里有一篇关于关于cgroup v1 empty notification在容器内失灵的邮件,提到了session回收的机制:https://listman.redhat.com/archives/libvir-list/2014-November/msg01090.html

使用dbus-monitor和strace观察的时候,dbus-monitor可以观察到session scope的UnitRemoved、session的SessionRemoved等事件在dbus上传输;不过此时挂上strace去观察systemd-logind却往往是正常工作的状态。
单独使用 dbus-monitor 但不用strace,发现残留的session后,再去查阅dbus-monitor的记录,发现在SessionRemoved消息之后出现的
path=/org/freedesktop/login1/user/_0; interface=org.freedesktop.DBus.Properties; member=PropertiesChanged
这个消息的消息体特别巨大,内含近六百个session的编号,应该是全量更新该用户下属的session列表,而不是差量更新。所以我认为session残留其实是个旧数据累积导致的systemd-logind的性能雪崩问题。

此外,dbus-monitor还观察到PolicyKit (polkit.service)服务启动的时候也有大量的session信息在dbus上传输,polkit服务启动后迟迟不能就绪。这个服务如不就绪,会导致systemctl重启服务的动作失败、reboot等命令也可能失败(https://github.com/systemd/systemd/blob/main/src/systemctl/systemctl-start-unit.c#L254 调用polkit_agent_open_maybe()函数)。这样就产生里一个循环依赖:清理残留session需要systemctl命令,systemctl命令需要polkit授权,而polkit也被残留的session给害死了无法正常工作。如果发生这种情况,就只好手工删除session文件,残留一些其他数据在内存里(参考https://github.com/systemd/systemd/blob/main/src/login/logind-session.c#L745)并借助外部机制进行整机重启。

另外,在大量执行systemctl stop session-XXX.scope的时候,也会给systemd-logind带来压力,因为每次开始或者结束session,都会把session号码写入UID对应的 /run/systemd/users/{uid} 文件内


每次变动都会导致这个文件全量覆写,代价也是很高昂的。在残存的session极多的情况下,很难缓过劲来。使用systemctl stop修复的耗时甚至会超过使用重启方法修复的耗时。
————————
另外两台因为oom整机重启, /run/ 是systemd管理的tmpfs,重启后内容丢失,躲过一劫。

经同事提醒,在另一台服务器上虽然也是用crond启动后台服务,但是却没有发生类似症状。我检查发现,该服务器上java进程所属的cgroup是system.slice/crond.slice ,继而发现其/etc/pam.d/password-auth文件被替换过,从一个包含pam_systemd.so的配置文件,变成了指向password-auth-ldap文件的符号链接。因为crond在变换执行身份的时候没有经过pam_systemd.so 所以也不会被systemd-logind 记录,不产生session,也自然不会有残留。这种更改误打误撞让这批服务器躲过一劫。但是由于公司内软件山寨的安装方式,到底谁改了这个配置文件、password-auth-ldap文件属于哪个软件,我费了很大劲才打听到是一个内部用于管理ssh登录权限的软件。

触发条件总结

  1. /etc/pam.d/crond遵守系统默认值,即:包含pam_systemd.so,就会把crond产生的子进程放到user session里去。具体到腾讯内的具体情况,如果安装了那个ssh登录权限管理软件,则可侥幸躲过一劫。
  2. cron任务比较频繁的时候,会产生大量的Session新建和销毁消息,伴随全量更新,对logind造成较大压力;在其未能处理完毕的时候下一次更新数据又来了,造成累积

改进建议

  1. 自研软件包也应该做成RPM包,实现安装过程标准化、文件可追踪(rpm –list)可验证(rpm –verify),可以追查哪个文件属于哪个软件包(rpm –query –file)
  2. 服务启动应该托管给systemd作为service unit,由systemd负责进程的启动、故障重启和关闭
  3. 应弃用容器作为服务器这种做法,规避cgroup v1 empty notification的问题

systemd的好处有:

  1. 通过cgroup可以知道Which Service Owns Which Processes,确保关闭服务时没有泄漏子进程请(参见《supervisor泄漏进程案例分析》);
  2. 通过SIGCHLD实现低代价(无额外进程vs. bash+ps+grep+grep)、实时(vs. crond的一分钟粒度)监测进程存活性
  3. 通过service unit file的声明式写法,使服务脱离用户级运行环境、脱离user.slice、system.slice/sshd.service和system.slice/crond.service的cgroup,拥有自己独立干净的启动条件,避免受到用户环境变量、rlimit等设置的干扰和传染

附清理方法:
必须判断进程是否存在,然后再清理残留的session scope,否则会误关闭进程。比如:
# grep name=systemd /proc/$(pidof java)/cgroup 1:name=systemd:/kubepods/pod480ee9e9-5ec9-11ea-bf78-6c0b84d57dd5/2b34cb158d5fa1db6f17d9099c3b5314f67223f6aeb71c3c6cedbafd84df0b24/user.slice/user-1002.slice/session-47639512.scope
这里java进程的cgroup是session-47639512.scope ,则/run/systemd/sessions/47639512 文件不应该被删除,也不应该执行 systemctl stop session-47639512.scope

然而,从session入手,推断其下属进程是比较困难的。
可以用下列命令判断,session文件存在但是cgroup已经消失的情况,以及cgroup存在但是内部不包含进程这两种情况,然后输出其路径或执行systemctl stop清理:
cd /sys/fs/cgroup/systemd/user.slice/; find /run/systemd/sessions -type f | xargs -n1 -i bash -c 'source {} 2>/dev/null ; DIR=user-$(getent passwd $USER|cut -d: -f3).slice/$SCOPE; ( test ! -d $DIR || test ! "cat $DIR/tasks" )&& echo stop $SCOPE || echo keep $DIR'

如果在C8机型上运行,上述cd的路径应参考/proc/1/cgroup的内容,修正为
`/sys/fs/cgroup/systemd/kubepods/pod{POD号}/{容器号}/user.slice/

Posted in 默认分类 | Tagged , , | Leave a comment

Reproducible builds 果然是很重要的

去年10月17日公司出了个事故,“幸好有我“,力挽狂澜。我觉得还是把具体内容脱敏,记录一下吧,免得将来忘了。

早晨test环境某个自研产品组件build成功,但是运行时错误日志里一堆“Broker hostname updated”等等,仿佛孔乙己说的话,让人听不懂。我刚开始没把这个当回事,请Kafka管理员去检查,几个小时都没查出原因来。

到了午后,各自研组件纷纷开始更新Live环境的版本,发布的都是前几天在Test环境已经测试定稿的版本,居然也出现了类似的问题。更狠的是,回滚居然也无效!

最终查实,有不少自研产品的build脚本里,使用wget下载master版本的librdkafka然后编译使用。而librdkafka恰在前一天(10月16日)被提交了很多修改。当时紧急解决的方法是使用librdkafka的上一个被明确标记了tag的v1.2.1版本代替master版本。

事后复盘处理过程,觉得当时的处理过程也有问题:所谓回滚其实并非直接使用旧版本编译出来的docker image,而是重新build了旧版本,而且编译时指定的是branch名字,而不是精确的commit id,这样的问题是无论外部代码(master版本的librdkafka)还是自研代码(注意branch有可能move forward哦),都不一定是原来的版本,这个行为其实并不能称作回滚,而是带有主观回滚意向的一次重新构建。

在这种情况下,讨论自研代码和外部库的质量,都是缺乏讨论基础的。这种讨论应该基于确定的版本。

说实话,我以前对于传说中的“Google把自研代码、外部库和工具链”全都纳入版本化管理,是有点嗤之以鼻的。当时我觉得这种做法,在处理多个自研软件的外部依赖相互冲突时,会带来额外的行政成本,在国内企业的KPI风格下,会导致相互推诿。经过这次力挽狂澜之后,觉得相互推诿其实是KPI至上主义导致的,而不是把所有代码都管起来导致的;不过是否真的要管这么大范围,尚有待商榷。

Posted in 默认分类 | Tagged , | Leave a comment

广东电信IPTV 华为EC6108v9c 的使用经验

前天(3月9日)终于拨乱反正把还剩下近一个月但网络“几乎能通”的垃圾天威视讯给弃用了,换了正经运营商中国电信广东深圳公司的上网服务,并且开通了IPTV。因为今年租的房子是正经居民房,就遭遇到了经典的“弱电箱距离电视机太远”的问题。

本来IPTV盒子可以支持Wi-Fi的,但它很快就强制自动升级,把本来支持的Wi-Fi联网方式给去掉了。

网上找到一个固件,保留了“粤TV”app,并且重新开启了Wi-Fi功能,且开放安装外来安卓软件。刷新说明里写着放在FAT32的U盘里,文件名叫update.zip,开盒子之后在bootloader过程中按左右键进入recovery,然后apply update from external storage即可。但我照做的时候,recovery提示没有找到升级包。

再搜,发现还有一个所谓按待机键进入recovery的,但我按不出来。妄want图to 拆盒子短接跳线,还从盒子里掉出一个虫子尸体,算是给祖师爷致敬了。

我想了想,可能这俩方法是不同时期的。于是我在当前recovery里apply update from backup刷回旧版了,(中间还操作了一步wipe dalvik cache,清除新版的升级包,避免旧版开机之强制刷新新版)然后再按待机键进入旧版recovery,刷了这个update.zip进去。

再说网络。官版固件有线网接入,目前是IPoE(其实就是特殊的DHCP)或者PPPoE认证的;直接DHCP无法获得IP地址。我改了一下光猫,把原来的VLAN 45上联业务,从PPPoE桥接模式(即:客户端自己PPPoE)改为PPPoE路由模式,并开放下行DHCP服务,再关联到SSID1上,用IPTV盒子去连这个Wi-Fi即可。理想情况下应该开多个SSID,但我这个光猫似乎不支持多个,所以这个2.4G only的Wi-Fi就给IPTV用,而自家其他通用设备使用的Wi-Fi,用了额外一个5GHz路由器从有线网口转出来。

Posted in 默认分类 | Tagged , | 1 Comment

浅谈用过的几台120相机

之前回老家的时候,发现家里多出一台海鸥4A相机,于是2019年要过来玩了几下,开始尝试玩120反转片。但是因为最初两卷拍暗了,以及经历其中有一张拍不下去只能过卷,所以对这台机器充满了怀疑,就搁置了,又买回一台Bronica ETRSi,然后过了几个月又买了一台Mamiya M645Pro。玩了几卷之后,觉得有必要浅谈一下这几台相机的使用感受,以便将来回忆和复习。

先简单介绍一下这几台相机:

海鸥4A:竖式外观一体式、两个镜头联合皮腔对焦、腰平取景、镜间快门、过卷联合快门镜头上弦(打开多重曝光之后可以只上弦)、自动停片(固定6*6画幅)、双反相机

Bronica ETRSi:“前后长”外观模块式、螺旋对焦、选配眼平测光取景器、过卷联合快门镜头上弦(打开多重曝光之后可以只上弦)、镜间快门、自动停片(固定6*4.5画幅)、单反相机,外观是“前后长”的

Mamiya M645Pro:“前后长”外观模块式、螺旋对焦、选配眼平非测光取景器、过卷联合快门上弦(打开多重曝光之后可以只上弦)、焦平面快门、自动停片(固定6*4.5画幅)、单反相机

120中画幅胶卷,宽度6厘米,所以成像尺寸的一个边固定为6厘米,不过120的优势是另一个边长度可变,有6*6、6*7、6*8、6*9、6*4.5等多种画幅。拍摄完成之后,胶卷会在另一个轴上重新卷起来,并用背纸贴封条保护起来,不同于135胶卷拍摄完成之后退回硬质保护壳的那种做法。

双反相机因为取景和拍摄的两个镜头上下排列(左右排列、眼平取景、无反光板那种相机叫旁轴,不叫双反),机身纵向尺寸较长,内部空间大,新旧胶卷可以上下排列;考虑到腰平取景,拍摄正方形或者landscape形状照片,导致画幅尺寸不超过6*6。

而模块化单反很多都是“前后长”的外观,从前到后分别是镜头、反光镜箱(即机身)、胶卷后背三个组件,反光镜日常呈45度反射,拍摄时上翻到水平,所以反光镜箱的上下方并没有更多的空间需求,比成像面积稍微大一点点而已;胶卷后背的尺寸一般也就和机身一致,导致胶卷在后背里需要经过一个omega形状的弯曲才能放置。很多人因为不理解这个扭曲的走向,把胶卷装反,用背纸去接收光,最终拍摄失败。

说说快门的区别:海鸥4A和Bronica ETRSi都是镜间快门,即快门是镜头的一部分。好处是可以在任意快门速度上实现闪光同步,坏处是镜头必须用与机身适配的型号,且安装拆卸镜头的时候要求机身镜头均已上弦。Mamiya M645是焦平面快门,取下胶卷后背即可看到机身后面的布帘,对这种相机,其实随便插个能完成光线调节的东西在前面都能拍。

反光镜的区别:海鸥4A、Bronica ETRSi拍摄之后都是反光镜不复位的,到下次上弦的时候镜子才重新回到45度;M645Pro拍摄之后就回镜,对于高频抓拍略有一点点好处。

多重曝光:ETRSi和M645Pro,其多重曝光开关其实就是个离合器,把胶卷后背的齿轮和机身齿轮分开,就能实现只上弦不过卷的功能;海鸥4A的我没看明白是咋回事,都隐藏在机器里面了。

闪光灯:海鸥4A只能用PC线接闪光灯,且没有合适的安装位置,你需要一位摄影助理;ETRSi可以通过PC线,也可以通过选配的过片手柄上的ISO热靴;M645Pro机身左侧自带一个ISO热靴,但是Sony MI接口闪光灯的接口略微长一点儿,而M645Pro的热靴插口最前面有“墙”,导致插不到规定深度,电路无法接通。

快门线:海鸥4A和ETRSi可以插机械快门线,也就是像自行车闸线似的那种,外面管道里面伸出钢丝那种;M645Pro需要一种特殊接口的电子快门线接通左侧的开关,在闲鱼我只看到一家卖这个的。

自拍倒计时:海鸥4A在镜头上;ETRSi在镜头上;M645Pro在快门按钮旋转到倒计时模式。

电池:海鸥4A不需要电池;ETRSi需要4LR44给电磁快门、测光取景器供电,没有电的时候提供一个固定速度的安全快门;M645Pro也需要4LR44电池,但是没电的时候就完全不能用了。

RB67,貌似中间的后背适配板是不带机械传动的,胶卷过片、镜头上弦、反光板翻转三个动作分离。

海鸥4A,网上流传说如果先上弦再改快门速度,会断弦,我还没敢试……

Posted in 默认分类 | Leave a comment