OpenTelemetryの計装ができているか確認する

OTLPを使うときによく opentelemetry.sdk.trace.export.BatchSpanProcessor を使うが、このモジュールには計装ができているかを簡単に確認できる SimpleSpanProcessor と ConsoleSpanExporter がある。

# ...
from opentelemetry.sdk.trace.export import BatchSpanProcessor, SimpleSpanProcessor, ConsoleSpanExporter

# ...
provider = TracerProvider()

if True:  # OTLP で出力するとき
    exporter = OTLPSpanExporter()
    processor = BatchSpanProcessor(exporter)
    provider.add_span_processor(processor)
else:  # デバッグ用にSpanをコンソール出力する
    provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))

このようにConsoleSpanExporterとSimpleSpanProcessorを組み合わせると、スパンを終了するとすぐにこのようにコンソールに表示される。

{
    "name": "select",
    "context": {
        "trace_id": "0x482e696dfeddb746339f93e26468871a",
        "span_id": "0x214fc3976d4a5031",
        "trace_state": "[]"
    },
    "kind": "SpanKind.CLIENT",
    "parent_id": null,
    "start_time": "2025-01-10T09:47:54.071241Z",
    "end_time": "2025-01-10T09:47:54.072727Z",
    "status": {
        "status_code": "UNSET"
    },
    "attributes": {
        "db.system": "mysql",
        "db.name": "mysql",
        "db.statement": "select help_topic_id, name from help_topic",
        "db.user": "root",
        "net.peer.name": "127.0.0.1",
        "net.peer.port": 3306
    },
    "events": [],
    "links": [],
    "resource": {
        "attributes": {
            "service.name": "mysqlclient-test"
        },
        "schema_url": ""
    }
}

ちゃんと計装できているか、取得するつもりのattributeが本当に取得できているかどうかの確認するとき、OpenTelemetry Collectorをローカルで動かすよりももっと手軽に使える。

太陽光発電の統合コストを読み解く

第7次エネルギー基本計画に向けて、2040年の発電コストの試算が発表された。

第5回 発電コスト検証ワーキンググループ|資源エネルギー庁

エネルギー基本計画は3年ごとに直されるが、今回の発電コスト試算で大きなトピックは、二酸化炭素排出コストと統合コストの一部が含まれるようになったことだ。

統合コストの一部を含む発電コスト(LCOE*)

太陽光発電の統合コストが非常に高くなっているのが目につくが、その理由と改善する見込みについて解説してみる。

前提となる電源構成について

この試算は、2040年の電力構成や系統について、一定の前提のもとに、発電所を追加する場合のコスト(限界コスト)を試算したものだ。

例えば、変動再エネ50%シナリオの限界コストは変動再エネ50%の状態から追加で発電所を作るときのコストであって、変動再エネ50%まで到達するためのコストではないことに注意が必要だ。2030年までに追加する太陽光発電にはこんなに大きな統合コストは必要ないので、この数字を元にいまから太陽光普及を止める必要は全くない。

各シナリオにおける発電設備容量は次のように設定されている。

変動再エネ割合ごとの設備容量

2023年時点で、太陽光発電の設備容量は80GW弱で、年間の発電量に占める割合は11%弱だ。年間の導入量は3GWから4GWまで落ち込んでいる。4GWを15年間追加した場合合計が140GWになる。4割のシナリオは今のペースで達成可能で、5割のシナリオはある程度のペースアップ、6割シナリオは現状の倍の導入ペースが必要になる。しかし6割シナリオでも太陽光の電力量としては3割程度で、他の再エネと合わせてギリギリ再エネ比率50%を達成できるかどうかと言ったところだ。

つまり、今の1.5倍程度までは増やしても太陽光が一番低コストだが、今の2倍まで増やすと原子力のコスト(膨れ上がらなかった場合)より高コストになり、さらに再エネ50%を目指すと急激に統合コストが膨れ上がるという試算になっている。

統合コストの内訳

統合コストの内訳を見てみよう。

「ディスパッチ等」は、要するに太陽光の変動を吸収するために火力発電が起動停止したり効率の悪い運転をするためのコストだ。そして充放電損失、出力抑制等は、揚水発電や蓄電池のロス分と、出力抑制で発電できないロスだ。6割シナリオではLCOEの3倍弱の発電ロス費用が発生しているので、追加された太陽光発電所の発電量の半分以上が捨てられる事になるのだろう。

コスト効率がいい再エネの発電抑制比率は1割程度までなので、50%以上を捨てるのは明らかに非現実的だ。この試算では系統蓄電池の量を固定して計算しているが、実際には変動再エネが増えてロスが増えてきたら系統蓄電池も増えるか併設されていくので、こんな事になる前に蓄電池が増えて統合コストは抑制されるだろう。

では、その蓄電池について、系統蓄電池と発電所併設蓄電池がどう計算されているか見ていこう。

系統蓄電池

揚水と系統蓄電池の設備容量

試算の前提として、系統蓄電池は20.9GW、揚水は29.8GWと設定されている。揚水はともかく、系統蓄電池の設定は適切だろうか?この※印にある「分野別投資戦略」はこれだ。

GX実現に向けた投資促進策を具体化する「分野別投資戦略」を取りまとめました (METI/経済産業省)

しかしこの蓄電池の資料を見ても、20.9GWの根拠は見つけられなかった。この資料では23年5月の系統蓄電池接続受付状況から2030年までの導入量を予測していた。

しかしその後系統蓄電池は大いに盛り上がり、契約受付が6.2GW、検討が88GWと、23年5月の状況から数倍に膨れ上がっている。25年4月からは、系統がいっぱいになったときに充電を止める仕組みを使う事で迅速に系統に接続するルールが始まり、一気に接続が増えていくだろう。2040年に20.9GWという想定はかなり保守的に思える。

この系統用蓄電池の保守的な想定が、LCOE*試算で太陽光の比率を上げると大半の電気を捨ててしまい大幅に統合コストが上がる原因になっている。

太陽光併設蓄電池

このコスト試算では蓄電池併設方の太陽光発電のコストも試算されていた。

これは変動再エネ5割シナリオを元にした試算だが、統合コストが14.9円/kWhから5.1円/kWhへと1/3に減っているものの、LCOEは8.5円から20.3円へと2倍以上に増えてしまっていて、トータルでは高コストになってしまっている。特に隣にある陸上風力のLCOEは蓄電池を併設してもほとんど上がってないのに対して異常に見える。

太陽光と風力で蓄電池のLCOEが大幅に違うのは、モデルプラントの設定による。

  • 風力のモデルプラントは30MWなのに対して、太陽光は250kW。(蓄電池は大きいほうが単価が下がる)
  • 風力は北海道などで周波数変動を抑制するために蓄電池併設を求められるケースがあり、1時間分の容量
  • 太陽光はほぼ全量を高く売れる時間帯に売るために3時間分の容量

しかし、それにしたって太陽光併設蓄電池のLCOEは高すぎないか?この数字の根拠はこのようになっている。

蓄電池自体の想定価格(5.7万円/kWh)も2040年にしては高いが、運営費も高すぎる。

蓄電池価格は1MW以上の大型発電所(メガソーラー)であれば今でももっと安くできるだろうし、小型蓄電池も最近Power X社のPower Cubeが登場したばかりなので、今後は上記の想定よりも大幅に前倒しで低価格化が進むだろう。 運営費の方も同じく、今後Power X社のX-PPAのような蓄電池併設太陽光のアグリゲーターが成長すれば、通常の太陽光の2倍以下に抑えられるのではないか。

そうして早い段階から大型の太陽光発電所で蓄電池併設が主流になれば、再エネ容量6割シナリオの統合費用はこの試算より大幅に抑えられるはずだ。

まとめ

  • 試算にある高い統合費用はもっと再エネを増やした未来(2040年)のもので、今から再エネ普及を抑制する必要は全くない
  • 再エネを増やしたら爆発的に統合コストが増えているのは、本来再エネとバランスよく増える蓄電池導入量を固定して計算したため
  • しかも、定置蓄電池(系統用蓄電池、再エネ併設蓄電池)は最近大幅に成長しているが、この試算ではかなり保守的に見積もられている
  • 3年後、6年後のエネルギー基本計画でまた発電コストが試算されるときには、蓄電池の普及によって再エネ50%の統合費用はもっと安くなるだろう

MariDB Connector/C がデフォルトでSSLを強制するようになった

MariaDB 11.4 がGAになり、SSLが自動で有効になるようになりました。合わせてMariaDB Connector/Cも3.4からデフォルトでSSLを利用し、かつサーバーの証明書を検証するようになりました。

これにより、MariaDB Connector/C 3.4以降のライブラリを利用してMySQLや古いMariaDBに接続する場合に、SSLで接続できないとか、証明書の検証に失敗するといった問題が発生するようになりました。 これらをリンクしたPythonのmysqlclientやRubyのmysql2のようなクライアントライブラリには続々と問題報告が上がってきています。

この問題に対処するいくつかの方法を紹介します。まずはConnectorをリンクしたライブラリの利用者ができることから。

  • MariaDB Connector/C 3.3を使う
  • ~/.my.cnf 等が利用されるなら、 [client] セクションに disable-ssl を書いてSSLを無効化するか、 disable-ssl-verify-server-cert を書いて証明書の検証を無効化する。
  • MariaDB Connector/C 3.4 をビルドする時のcmakeのオプションに -DDEFAULT_SSL_VERIFY_SERVER_CERT=0 を追加する。(証明書の検証をデフォルトでオフにできるだけで、SSLをデフォルトでオフにはならない) (参考)
  • MariaDB Connector/C 3.4.2 (未リリース)以降を使い、 MARIADB_TLS_DISABLE_PEER_VERIFICATION 環境変数を設定する。(同じく検証のみがオフになる)(参考)

また、MariaDB Connector/CやMySQL Connector/Cを利用するクライアントライブラリを開発している人向けのヒントも紹介します。

  • MySQLMYSQL_OPT_SSL_MODE オプションを持つが、MariaDBには存在しない。
  • MySQLは8.0で MYSQL_OPT_SSL_ENFORCE オプションを削除したが、MariaDBでは現役で、SSLの有効、無効を切り替える手段。
  • Connector/CがデフォルトでSSLを無効だという前提が崩れたので、SSLを有効にするオプションだけでなく、無効にするオプションを実装する必要がある。
    • 後方互換性やMySQLとの一貫性を考えると、ユーザーがSSL有効を指定しなかった場合は MYSQL_OPT_SSL_ENFORCE を 0 にしてSSL無効化する方法がある。
    • MariaDBの「デフォルトでセキュアに」というポリシーを尊重するのであれば、ユーザーがSSL無効化や検証無効化を明示的に指定したときに、デフォルトで無効になっている事を前提にせずちゃんと無効化するようにする。

小さい文字列からASCII外のバイトを見つけたい

Pythonの内部処理で、与えられたUTF-8の文字列がASCII文字のみを含むかをテストするコードを書いています。

size_t型が8バイトの時、ポインタを char* から size_t* にキャストして、0x8080808080808080ullと論理積を取れば8バイトを一度に処理できます。 単純化すると次のようなコードになります。

#define ASCII_MASK 0x8080808080808080ull

size_t find_first_nonascii(const char *start, const char *end)
{
    const char *p = start;

    // 8バイトずつ処理
    while (p <= end - 8) {
        if (*(size_t*)p & ASCII_MASK) {
            break;
        }
        p += 8;
    }
    // 1バイトずつ処理
    while (p < end) {
        if (*p & 0x80) {
            return p-start;
        }
        p++;
    }
    return end-start;
}

1バイトずつ処理する部分が気になります。これは最下位ビットから1になっているビットを探して位置を返してくれるGCC/clangのビルトイン関数 __builtin_ctzll を使えば、前半部分をこのように高速化できます。

    while (p <= end - 8) {
        size_t x = *(size_t*)p & ASCII_MASK;
        if (x) {
            // p[0] がASCII外の時、 ctzll(0x80) == 7, (7-7)/8 == 0
            // p[1] がASCII外の時、 ctzll(0x8000) == 15, (15-7)/8 == 1
            return p-start + (__builtin_ctzll(x) - 7) / 8;
        }
        p += 8;
    }

しかし問題は後半部分です。 8バイト未満の端数部分をsize_tに格納できれば同じ高速化ができます。たとえば次のようになります。

    // (end-p)が3のとき、 3*8=24, 1ull << 24 = 0x1000000, 0x1000000-1 = 0xffffff.
    // これとmaskしてやれば、4バイト以降は無視できる。
    size_t x = *(size_t*)p & ((1ull << (end - p) * 8) - 1) & ASCII_MASK;
    if (x) {
        return p - start + (__builtin_ctzll(x) - 7) / 8;
    }
    return end - start;

これでバイト数*2回だった分岐が1回に減らせてめでたしめでたし、、、なのですが、残念ながら入力の文字列の範囲外を読んでからマスクしているだけなので、範囲外アクセスが発生しています。 試しに clang の AddressSanitizer を使ってみるとエラーが出てしまいました。

アライメントさえ合っていれば、8バイトのreadがセグメンテーション違反を起こすことは無いと思いますが、範囲外アクセスはやはり避けたいです。 そこで次のように switch 文を書いてみました。

    size_t u = (size_t)(p[0]);
    switch (end - p) {
        default:
            u |= (size_t)(p[7]) << 56ull;
        // fall through
        case 7:
            u |= (size_t)(p[6]) << 48ull;
        // fall through
        case 6:
            u |= (size_t)(p[5]) << 40ull;
        // fall through
        case 5:
            u |= (size_t)(p[4]) << 32ull;
        // fall through
        case 4:
            u |= (size_t)(p[3]) << 24;
        // fall through
        case 3:
            u |= (size_t)(p[2]) << 16;
        // fall through
        case 2:
            u |= (size_t)(p[1]) << 8;
            break;
        case 1:
            break;
    }
    if (u & ASCII_CHAR_MASK) {
        return p - start + (__builtin_ctzll(u & ASCII_CHAR_MASK) - 7) / 8;
    }

これで範囲外アクセスはなくなりました。 clang が生成するアセンブリ(intel形式)は次のような感じになります。

...
                u |= (size_t)(p[7]) << 56ull;
 147:   44 0f b6 59 07          movzx  r11d,BYTE PTR [rcx+0x7]
 14c:   49 c1 e3 38             shl    r11,0x38
 150:   4d 09 da                or     r10,r11
                u |= (size_t)(p[6]) << 48ull;
 153:   44 0f b6 59 06          movzx  r11d,BYTE PTR [rcx+0x6]
 158:   49 c1 e3 30             shl    r11,0x30
 15c:   4d 09 da                or     r10,r11
                u |= (size_t)(p[5]) << 40ull;
 15f:   44 0f b6 59 05          movzx  r11d,BYTE PTR [rcx+0x5]
 164:   49 c1 e3 28             shl    r11,0x28
 168:   4d 09 da                or     r10,r11
                u |= (size_t)(p[4]) << 32ull;
...

1バイトずつリードして、左シフトして、ORしてますね。switchのジャンプテーブルは使われているので分岐を減らす目的は達成できました。

しかし、アライメントを保証することで、範囲外リードを安全に行うような方法があればそっちのほうがいいので、その方法を現在探しています。

"Missing Signed-By in the sources.list(5) entry for" を解消する

開発機をUbuntu 24.04 にアップデートしてしばらく経って、 apt update したときに表示される次のwarningが今更気になった。

N: Missing Signed-By in the sources.list(5) entry for 'http://ftp.riken.go.jp/Linux/ubuntu'

試しに /etc/apt/sources.list をのぞいてみると、次の1行になっていた。

# Ubuntu sources have moved to /etc/apt/sources.list.d/ubuntu.sources

/etc/apt/sources.list.d には third-party.sources, ubuntu.sources の2つのファイルがあった。

/etc/apt/sources.list.d$ cat ubuntu.sources
Types: deb
URIs: http://security.ubuntu.com/ubuntu/
Suites: noble-security
Components: main restricted universe multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg

/etc/apt/sources.list.d$ cat third-party.sources
Types: deb
URIs: http://ftp.riken.go.jp/Linux/ubuntu/
Suites: noble noble-updates noble-backports
Components: main restricted universe multiverse

Ubuntuをアップグレードした時に sources.list 内のソースが、公式のものは ubuntu.sources に、ミラーのものは third-party.sources に分割されたようだ。

third-party.sources には Signed-By: が付いていないが、本家のミラーなので ubuntu.sources の Signed-By: をコピーしてみたところ、冒頭のwarningが消えた。

このブログに乗せているコードは引用を除き CC0 1.0 で提供します。